From dd1da6b1a429cf2b110138aa7a0cdad965138705 Mon Sep 17 00:00:00 2001 From: Harris Date: Mon, 7 Feb 2022 17:06:27 +1100 Subject: [PATCH] Add a global search (#834) * feat: modifying search functionalities to include contacts * feat: add global search UI input layouts and color attributes * feat: add global search repository and model content * feat: adding diff callbacks and wiring up global search vm to views * feat: adding scroll to message, figuring out new query for recipient thread search * feat: messing with the search and highlighting functionality after wiring up bindings * fix: compile error from merge * fix: gradlew build errors * feat: filtering contacts by existing un-archived threads * refactor: prevent note to self breaking, update queries and logic in search repo to include member->group reverse searches * feat: adding home screen new redesigns for search * feat: replacing designs and adding new group subtitle text * feat: small design improvements and incrementing gradle build number to install on device * feat: add scrollbars for search * feat: replace isVisible for cancel button now that GlobalSearchInputLayout.kt replaces header * refactor: all queries are debounced not just all but 2 char * refactor: remove visibility modifiers for cancel icon * refactor: use simplified non-db and context related models in display, remove db get group members call from binding data * fix: use threadId instead of group's address * refactor: better close on cancel, removing only yourself from group member list in open groups * refactor: seed view back to inflated on create and visibility for empty placeholder and seed view text * refactor: fixing build issues and new designs for message list * refactor: use dynamic limit * refactor: include raw session ID string search for non-empty threads * fix: build lint errors * fix: build issues * feat: add in path to the settings activity * refactor: remove wildcard imports --- app/build.gradle | 3 +- app/src/androidTest/AndroidManifest.xml | 2 +- .../loki/messenger/HomeActivityTests.kt | 5 +- .../securesms/audio/AudioCodec.java | 8 +- .../securesms/contacts/ContactAccessor.java | 19 +- .../conversation/v2/ConversationActivityV2.kt | 18 +- .../v2/messages/LinkPreviewView.kt | 2 +- .../v2/messages/VisibleMessageContentView.kt | 2 +- .../v2/messages/VisibleMessageView.kt | 2 +- .../conversation/v2/search/SearchViewModel.kt | 19 +- .../securesms/database/GroupDatabase.java | 26 ++- .../securesms/database/LokiAPIDatabase.kt | 2 +- .../securesms/database/MmsSmsDatabase.java | 4 +- .../securesms/database/SearchDatabase.java | 9 +- .../database/SessionContactDatabase.kt | 28 ++- .../securesms/database/ThreadDatabase.java | 17 +- .../groups/JoinPublicChatActivity.kt | 2 +- .../securesms/home/HomeActivity.kt | 182 +++++++++++++++--- .../home/search/GlobalSearchAdapter.kt | 129 +++++++++++++ .../home/search/GlobalSearchAdapterUtils.kt | 160 +++++++++++++++ .../home/search/GlobalSearchInputLayout.kt | 88 +++++++++ .../home/search/GlobalSearchResult.kt | 34 ++++ .../home/search/GlobalSearchViewModel.kt | 69 +++++++ .../mediasend/MediaPickerItemFragment.java | 4 +- .../notifications/BackgroundPollWorker.kt | 2 +- .../LokiPushNotificationManager.kt | 4 +- .../CorrectedPreferenceFragment.java | 2 +- .../securesms/preferences/SettingsActivity.kt | 10 + .../securesms/search/SearchModule.kt | 33 ++++ .../securesms/search/SearchRepository.java | 144 +++++++++++--- .../securesms/search/model/SearchResult.java | 15 +- .../thoughtcrime/securesms/util/BackupUtil.kt | 2 +- .../securesms/util/DateUtils.java | 4 +- .../thoughtcrime/securesms/util/IP2Country.kt | 4 +- .../securesms/util/SearchUtil.java | 5 +- .../ic_outline_bookmark_border_24.xml | 10 + app/src/main/res/drawable/ic_session.xml | 27 +++ .../main/res/drawable/search_background.xml | 6 + app/src/main/res/font/roboto_medium.ttf | Bin 0 -> 168644 bytes app/src/main/res/layout/activity_home.xml | 67 +++++-- app/src/main/res/layout/activity_settings.xml | 39 +++- app/src/main/res/layout/alert_view.xml | 7 +- app/src/main/res/layout/camera_fragment.xml | 6 +- .../main/res/layout/delivery_status_view.xml | 5 +- .../main/res/layout/emoji_display_item.xml | 2 +- app/src/main/res/layout/media_keyboard.xml | 4 +- .../res/layout/media_view_remove_button.xml | 17 +- .../res/layout/mediapicker_folder_item.xml | 6 +- .../res/layout/mediapicker_media_item.xml | 2 +- .../main/res/layout/mediasend_activity.xml | 8 +- .../main/res/layout/mediasend_fragment.xml | 2 +- app/src/main/res/layout/quote_view.xml | 4 +- .../main/res/layout/seed_reminder_stub.xml | 6 - app/src/main/res/layout/thumbnail_view.xml | 2 +- .../res/layout/transfer_controls_view.xml | 2 +- app/src/main/res/layout/view_conversation.xml | 6 +- .../res/layout/view_global_search_header.xml | 16 ++ .../res/layout/view_global_search_input.xml | 58 ++++++ .../res/layout/view_global_search_result.xml | 111 +++++++++++ app/src/main/res/menu/menu_home.xml | 16 ++ app/src/main/res/values-fi-rFI/strings.xml | 2 +- app/src/main/res/values-fi/strings.xml | 2 +- app/src/main/res/values-hi-rIN/strings.xml | 1 - .../main/res/values-notnight-v21/themes.xml | 4 + app/src/main/res/values-sq-rAL/strings.xml | 1 - app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/themes.xml | 3 + app/src/main/res/values/values.xml | 3 + .../thoughtcrime/securesms/BaseUnitTest.java | 78 -------- .../CursorRecyclerViewAdapterTest.java | 2 +- .../securesms/jobs/FastJobStorageTest.java | 48 +++-- .../securesms/util/ListPartitionTest.java | 3 +- .../securesms/util/Rfc5724UriTest.java | 3 +- gradle.properties | 2 +- libsession/build.gradle | 13 +- .../messaging/jobs/BatchMessageReceiveJob.kt | 2 +- .../sending_receiving/MessageSender.kt | 2 +- .../ReceivedMessageHandler.kt | 2 +- .../notifications/PushNotificationAPI.kt | 4 +- .../pollers/ClosedGroupPollerV2.kt | 6 +- .../org/session/libsession/utilities/Util.kt | 3 +- libsession/src/main/res/values/arrays.xml | 53 ----- .../org/session/libsignal/ExampleUnitTest.kt | 17 -- 84 files changed, 1370 insertions(+), 376 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/search/SearchModule.kt create mode 100644 app/src/main/res/drawable/ic_outline_bookmark_border_24.xml create mode 100644 app/src/main/res/drawable/ic_session.xml create mode 100644 app/src/main/res/drawable/search_background.xml create mode 100644 app/src/main/res/font/roboto_medium.ttf delete mode 100644 app/src/main/res/layout/seed_reminder_stub.xml create mode 100644 app/src/main/res/layout/view_global_search_header.xml create mode 100644 app/src/main/res/layout/view_global_search_input.xml create mode 100644 app/src/main/res/layout/view_global_search_result.xml create mode 100644 app/src/main/res/menu/menu_home.xml delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/BaseUnitTest.java delete mode 100644 libsignal/src/test/java/org/session/libsignal/ExampleUnitTest.kt diff --git a/app/build.gradle b/app/build.gradle index 4e91ee123..40a9e4f0e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -130,6 +130,7 @@ dependencies { testImplementation 'androidx.test:core:1.3.0' testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" // Core library androidTestImplementation 'androidx.test:core:1.4.0' @@ -156,7 +157,7 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.4' } -def canonicalVersionCode = 242 +def canonicalVersionCode = 248 def canonicalVersionName = "1.11.14" def postFixSize = 10 diff --git a/app/src/androidTest/AndroidManifest.xml b/app/src/androidTest/AndroidManifest.xml index 68f81f6f8..deab87dd6 100644 --- a/app/src/androidTest/AndroidManifest.xml +++ b/app/src/androidTest/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="network.loki.messenger.test"> diff --git a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt index 46db01b13..e59b49669 100644 --- a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt +++ b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt @@ -15,6 +15,7 @@ import androidx.test.platform.app.InstrumentationRegistry import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable import network.loki.messenger.util.NewConversationButtonDrawableMatcher.Companion.newConversationButtonWithDrawable import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.not import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -73,7 +74,7 @@ class HomeActivityTests { onView(allOf(withId(R.id.button), isDescendantOfA(withId(R.id.seedReminderView)))).perform(ViewActions.click()) onView(withId(R.id.copyButton)).perform(ViewActions.click()) pressBack() - onView(withId(R.id.seedReminderView)).check(matches(withEffectiveVisibility(Visibility.GONE))) + onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed()))) } @Test @@ -85,7 +86,7 @@ class HomeActivityTests { @Test fun testIsVisible_alreadyDismissed_seedView() { setupLoggedInState(hasViewedSeed = true) - onView(withId(R.id.seedReminderView)).check(doesNotExist()) + onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed()))) } @Test diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java index ff3b0b809..699e9ba97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java @@ -35,12 +35,10 @@ public class AudioCodec { public AudioCodec() throws IOException { this.bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); - this.audioRecord = createAudioRecord(this.bufferSize); this.mediaCodec = createMediaCodec(this.bufferSize); - - this.mediaCodec.start(); - try { + this.audioRecord = createAudioRecord(this.bufferSize); + this.mediaCodec.start(); audioRecord.startRecording(); } catch (Exception e) { Log.w(TAG, e); @@ -167,7 +165,7 @@ public class AudioCodec { return adtsHeader; } - private AudioRecord createAudioRecord(int bufferSize) { + private AudioRecord createAudioRecord(int bufferSize) throws SecurityException { return new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize * 10); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java index ecd94cff4..d560247fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -66,25 +66,18 @@ public class ContactAccessor { public List getNumbersForThreadSearchFilter(Context context, String constraint) { LinkedList numberList = new LinkedList<>(); - GroupDatabase.Reader reader = null; GroupRecord record; - - try { - reader = DatabaseComponent.get(context).groupDatabase().getGroupsFilteredByTitle(constraint); - + try (GroupDatabase.Reader reader = DatabaseComponent.get(context).groupDatabase().getGroupsFilteredByTitle(constraint)) { while ((record = reader.getNext()) != null) { numberList.add(record.getEncodedId()); } - } finally { - if (reader != null) - reader.close(); } - if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) && - !numberList.contains(TextSecurePreferences.getLocalNumber(context))) - { - numberList.add(TextSecurePreferences.getLocalNumber(context)); - } +// if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) && +// !numberList.contains(TextSecurePreferences.getLocalNumber(context))) +// { +// numberList.add(TextSecurePreferences.getLocalNumber(context)); +// } return numberList; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index a41a2c373..fc181d79c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -133,6 +133,8 @@ import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.toPx import java.util.Locale import java.util.concurrent.ExecutionException +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import kotlin.math.abs import kotlin.math.max @@ -249,12 +251,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private val documentButton by lazy { InputBarButton(this, R.drawable.ic_document_small_dark, hasOpaqueBackground = true) } private val libraryButton by lazy { InputBarButton(this, R.drawable.ic_baseline_photo_library_24, hasOpaqueBackground = true) } private val cameraButton by lazy { InputBarButton(this, R.drawable.ic_baseline_photo_camera_24, hasOpaqueBackground = true) } + private val messageToScrollTimestamp = AtomicLong(-1) + private val messageToScrollAuthor = AtomicReference(null) // region Settings companion object { // Extras const val THREAD_ID = "thread_id" const val ADDRESS = "address" + const val SCROLL_MESSAGE_ID = "scroll_message_id" + const val SCROLL_MESSAGE_AUTHOR = "scroll_message_author" // Request codes const val PICK_DOCUMENT = 2 const val TAKE_PHOTO = 7 @@ -272,6 +278,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe super.onCreate(savedInstanceState, isReady) binding = ActivityConversationV2Binding.inflate(layoutInflater) setContentView(binding.root) + // messageIdToScroll + messageToScrollTimestamp.set(intent.getLongExtra(SCROLL_MESSAGE_ID, -1)) + messageToScrollAuthor.set(intent.getParcelableExtra(SCROLL_MESSAGE_AUTHOR)) val thread = threadDb.getRecipientForThreadId(viewModel.threadId) if (thread == null) { Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() @@ -351,6 +360,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun onLoadFinished(loader: Loader, cursor: Cursor?) { adapter.changeCursor(cursor) + if (cursor != null) { + val messageTimestamp = messageToScrollTimestamp.getAndSet(-1) + val author = messageToScrollAuthor.getAndSet(null) + if (author != null && messageTimestamp >= 0) { + jumpToMessage(author, messageTimestamp, null) + } + } } override fun onLoaderReset(cursor: Loader) { @@ -1296,7 +1312,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun resendMessage(messages: Set) { - messages.forEach { messageRecord -> + messages.iterator().forEach { messageRecord -> ResendMessageUtilities.resend(messageRecord) } endActionMode() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt index 0f8431c64..fbf40fe52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -90,7 +90,7 @@ class LinkPreviewView : LinearLayout { } // intersectedModalSpans should only be a list of one item val hitSpans = bodyTextView.getIntersectedModalSpans(hitRect) - hitSpans.forEach { span -> + hitSpans.iterator().forEach { span -> span.onClick(bodyTextView) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 7b1b06be2..63cfbbda2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -214,7 +214,7 @@ class VisibleMessageContentView : LinearLayout { val body = getBodySpans(context, message, searchQuery) binding.bodyTextView.text = body onContentClick.add { e: MotionEvent -> - binding.bodyTextView.getIntersectedModalSpans(e).forEach { span -> + binding.bodyTextView.getIntersectedModalSpans(e).iterator().forEach { span -> span.onClick(binding.bodyTextView) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 38723d239..615c5ff09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -390,7 +390,7 @@ class VisibleMessageView : LinearLayout { } fun onContentClick(event: MotionEvent) { - binding.messageContentView.onContentClick.forEach { clickHandler -> clickHandler.invoke(event) } + binding.messageContentView.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) } } private fun onPress(event: MotionEvent) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt index 2a7dd099b..48bb731c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt @@ -11,6 +11,7 @@ import org.session.libsession.utilities.concurrent.SignalExecutors import org.thoughtcrime.securesms.contacts.ContactAccessor import org.thoughtcrime.securesms.database.CursorList import org.thoughtcrime.securesms.database.SearchDatabase +import org.thoughtcrime.securesms.database.SessionContactDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.search.SearchRepository import org.thoughtcrime.securesms.search.model.MessageResult @@ -20,14 +21,11 @@ import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( - @ApplicationContext context: Context, - searchDb: SearchDatabase, - threadDb: ThreadDatabase + private val searchRepository: SearchRepository ) : ViewModel() { - private val searchRepository: SearchRepository - private val result: CloseableLiveData - private val debouncer: Debouncer + private val result: CloseableLiveData = CloseableLiveData() + private val debouncer: Debouncer = Debouncer(500) private var firstSearch = false private var searchOpen = false private var activeQuery: String? = null @@ -107,13 +105,4 @@ class SearchViewModel @Inject constructor( } } - init { - result = CloseableLiveData() - debouncer = Debouncer(500) - searchRepository = SearchRepository(context, - searchDb, - threadDb, - ContactAccessor.getInstance(), - SignalExecutors.SERIAL) - } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index a9042ed39..aa80bdb17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -29,6 +29,7 @@ import org.session.libsignal.database.LokiOpenGroupDatabaseProtocol; import java.io.Closeable; import java.security.SecureRandom; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -111,7 +112,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt } } - Optional getGroup(Cursor cursor) { + public Optional getGroup(Cursor cursor) { Reader reader = new Reader(cursor); return Optional.fromNullable(reader.getCurrent()); } @@ -146,6 +147,29 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt return groups; } + public Cursor getGroupsFilteredByMembers(List members) { + if (members == null || members.isEmpty()) { + return null; + } + + String[] queriesValues = new String[members.size()]; + + StringBuilder queries = new StringBuilder(); + for (int i=0; i < members.size(); i++) { + boolean isEnd = i == (members.size() - 1); + queries.append(MEMBERS + " LIKE ?"); + queriesValues[i] = "%"+members.get(i)+"%"; + if (!isEnd) { + queries.append(" OR "); + } + } + + return databaseHelper.getReadableDatabase().query(TABLE_NAME, null, + queries.toString(), + queriesValues, + null, null, null); + } + public @NonNull List getGroupMembers(String groupId, boolean includeSelf) { List
members = getCurrentMembers(groupId, false); List recipients = new LinkedList<>(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt index ab0e0ba0f..7f32fab1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt @@ -450,7 +450,7 @@ private inline fun wrap(x: T): Array { private fun wrap(x: Map): ContentValues { val result = ContentValues(x.size) - x.forEach { result.put(it.key, it.value) } + x.iterator().forEach { result.put(it.key, it.value) } return result } // endregion \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 54d07afe0..f9d524010 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -139,7 +139,7 @@ public class MmsSmsDatabase extends Database { try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) { cursor.moveToFirst(); - return cursor.getLong(cursor.getColumnIndex(MmsSmsColumns.ID)); + return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID)); } } @@ -157,7 +157,7 @@ public class MmsSmsDatabase extends Database { try { return cursor != null ? cursor.getCount() : 0; } finally { - if (cursor != null) cursor.close();; + if (cursor != null) cursor.close(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java index 798f34e00..37efc9a43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SearchDatabase.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.database; import android.content.Context; + import androidx.annotation.NonNull; import com.annimon.stream.Stream; @@ -8,8 +9,8 @@ import com.annimon.stream.Stream; import net.sqlcipher.Cursor; import net.sqlcipher.database.SQLiteDatabase; -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.session.libsession.utilities.Util; +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import java.util.List; @@ -80,7 +81,7 @@ public class SearchDatabase extends Database { "INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " + "WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " + "ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " + - "LIMIT 500"; + "LIMIT ?"; private static final String MESSAGES_FOR_THREAD_QUERY = "SELECT " + @@ -115,7 +116,9 @@ public class SearchDatabase extends Database { SQLiteDatabase db = databaseHelper.getReadableDatabase(); String prefixQuery = adjustQuery(query); - Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery }); + int queryLimit = Math.min(query.length()*50,500); + + Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery, String.valueOf(queryLimit) }); setNotifyConverationListListeners(cursor); return cursor; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt index 9bcf94ec1..ef9f0cc38 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionContactDatabase.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context +import androidx.core.database.getStringOrNull import net.sqlcipher.Cursor import org.session.libsession.messaging.contacts.Contact import org.session.libsignal.utilities.Base64 @@ -73,7 +74,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da notifyConversationListListeners() } - private fun contactFromCursor(cursor: Cursor): Contact { + fun contactFromCursor(cursor: Cursor): Contact { val sessionID = cursor.getString(sessionID) val contact = Contact(sessionID) contact.name = cursor.getStringOrNull(name) @@ -87,4 +88,29 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da contact.isTrusted = cursor.getInt(isTrusted) != 0 return contact } + + fun contactFromCursor(cursor: android.database.Cursor): Contact { + val sessionID = cursor.getString(cursor.getColumnIndexOrThrow(sessionID)) + val contact = Contact(sessionID) + contact.name = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(name)) + contact.nickname = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(nickname)) + contact.profilePictureURL = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureURL)) + contact.profilePictureFileName = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureFileName)) + cursor.getStringOrNull(cursor.getColumnIndexOrThrow(profilePictureEncryptionKey))?.let { + contact.profilePictureEncryptionKey = Base64.decode(it) + } + contact.threadID = cursor.getLong(cursor.getColumnIndexOrThrow(threadID)) + contact.isTrusted = cursor.getInt(cursor.getColumnIndexOrThrow(isTrusted)) != 0 + return contact + } + + fun queryContactsByName(constraint: String): Cursor { + return databaseHelper.readableDatabase.query( + sessionContactTable, null, " $name LIKE ? OR $nickname LIKE ?", arrayOf( + "%$constraint%", + "%$constraint%" + ), + null, null, null + ) + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 058345dcf..84c7de34e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -339,6 +339,19 @@ public class ThreadDatabase extends Database { } + public Cursor searchConversationAddresses(String addressQuery) { + if (addressQuery == null || addressQuery.isEmpty()) { + return null; + } + + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String selection = TABLE_NAME + "." + ADDRESS + " LIKE ? AND " + TABLE_NAME + "." + MESSAGE_COUNT + " != 0"; + String[] selectionArgs = new String[]{addressQuery+"%"}; + String query = createQuery(selection, 0); + Cursor cursor = db.rawQuery(query, selectionArgs); + return cursor; + } + public Cursor getFilteredConversationList(@Nullable List
filter) { if (filter == null || filter.size() == 0) return null; @@ -706,14 +719,14 @@ public class ThreadDatabase extends Database { long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)); int unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT)); long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE)); - boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0; + boolean archived = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.ARCHIVED)) != 0; int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS)); int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.DELIVERY_RECEIPT_COUNT)); int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ_RECEIPT_COUNT)); long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN)); long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN)); Uri snippetUri = getSnippetUri(cursor); - boolean pinned = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.IS_PINNED)) != 0; + boolean pinned = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.IS_PINNED)) != 0; if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { readReceiptCount = 0; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt index 92802632b..ba87e8706 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt @@ -200,7 +200,7 @@ class EnterChatURLFragment : Fragment() { private fun populateDefaultGroups(groups: List) { binding.defaultRoomsGridLayout.removeAllViews() binding.defaultRoomsGridLayout.useDefaultMargins = false - groups.forEach { defaultGroup -> + groups.iterator().forEach { defaultGroup -> val chip = layoutInflater.inflate(R.layout.default_group_chip, binding.defaultRoomsGridLayout, false) as Chip val drawable = defaultGroup.image?.let { bytes -> val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 688219916..035ceb971 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -7,10 +7,9 @@ import android.content.Intent import android.content.IntentFilter import android.database.Cursor import android.os.Bundle -import android.text.Spannable import android.text.SpannableString -import android.text.style.ForegroundColorSpan import android.widget.Toast +import androidx.activity.viewModels import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.lifecycle.Observer @@ -20,20 +19,24 @@ import androidx.loader.content.Loader import androidx.localbroadcastmanager.content.LocalBroadcastManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.databinding.ActivityHomeBinding -import network.loki.messenger.databinding.SeedReminderStubBinding import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.ProfilePictureModifiedEvent import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.ApplicationContext @@ -43,6 +46,7 @@ import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord @@ -51,32 +55,42 @@ import org.thoughtcrime.securesms.dms.CreatePrivateChatActivity import org.thoughtcrime.securesms.groups.CreateClosedGroupActivity import org.thoughtcrime.securesms.groups.JoinPublicChatActivity import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter +import org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout +import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests -import org.thoughtcrime.securesms.notifications.MarkReadReceiver import org.thoughtcrime.securesms.onboarding.SeedActivity import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.IP2Country +import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.disableClipping -import org.thoughtcrime.securesms.util.getColorWithID import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.show import java.io.IOException import javax.inject.Inject @AndroidEntryPoint -class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, - SeedReminderViewDelegate, NewConversationButtonSetViewDelegate, LoaderManager.LoaderCallbacks { +class HomeActivity : PassphraseRequiredActionBarActivity(), + ConversationClickListener, + SeedReminderViewDelegate, + NewConversationButtonSetViewDelegate, + LoaderManager.LoaderCallbacks, + GlobalSearchInputLayout.GlobalSearchInputLayoutListener { private lateinit var binding: ActivityHomeBinding private lateinit var glide: GlideRequests private var broadcastReceiver: BroadcastReceiver? = null @Inject lateinit var threadDb: ThreadDatabase + @Inject lateinit var mmsSmsDatabase: MmsSmsDatabase @Inject lateinit var recipientDatabase: RecipientDatabase @Inject lateinit var groupDatabase: GroupDatabase + @Inject lateinit var textSecurePreferences: TextSecurePreferences + + private val globalSearchViewModel by viewModels() private val publicKey: String get() = TextSecurePreferences.getLocalNumber(this)!! @@ -85,6 +99,46 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis HomeAdapter(context = this, cursor = threadDb.conversationList, listener = this) } + private val globalSearchAdapter = GlobalSearchAdapter { model -> + when (model) { + is GlobalSearchAdapter.Model.Message -> { + val threadId = model.messageResult.threadId + val timestamp = model.messageResult.receivedTimestampMs + val author = model.messageResult.messageRecipient.address + + val intent = Intent(this, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.THREAD_ID, threadId) + intent.putExtra(ConversationActivityV2.SCROLL_MESSAGE_ID, timestamp) + intent.putExtra(ConversationActivityV2.SCROLL_MESSAGE_AUTHOR, author) + push(intent) + } + is GlobalSearchAdapter.Model.SavedMessages -> { + val intent = Intent(this, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(model.currentUserPublicKey)) + push(intent) + } + is GlobalSearchAdapter.Model.Contact -> { + val address = model.contact.sessionID + + val intent = Intent(this, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(address)) + push(intent) + } + is GlobalSearchAdapter.Model.GroupConversation -> { + val groupAddress = Address.fromSerialized(model.groupRecord.encodedId) + val threadId = threadDb.getThreadIdIfExistsFor(Recipient.from(this, groupAddress, false)) + if (threadId >= 0) { + val intent = Intent(this, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.THREAD_ID, threadId) + push(intent) + } + } + else -> { + Log.d("Loki", "callback with model: $model") + } + } + } + // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) @@ -98,28 +152,28 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis // Set up toolbar buttons binding.profileButton.glide = glide binding.profileButton.setOnClickListener { openSettings() } - binding.pathStatusViewContainer.disableClipping() - binding.pathStatusViewContainer.setOnClickListener { showPath() } + binding.searchViewContainer.setOnClickListener { + binding.globalSearchInputLayout.requestFocus() + } + binding.sessionToolbar.disableClipping() // Set up seed reminder view val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) if (!hasViewedSeed) { - binding.seedReminderStub.setOnInflateListener { _, inflated -> - val stubBinding = SeedReminderStubBinding.bind(inflated) - val seedReminderViewTitle = SpannableString("You're almost finished! 80%") // Intentionally not yet translated - seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - stubBinding.seedReminderView.title = seedReminderViewTitle - stubBinding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1) - stubBinding.seedReminderView.setProgress(80, false) - stubBinding.seedReminderView.delegate = this@HomeActivity - } - binding.seedReminderStub.inflate() + binding.seedReminderView.isVisible = true + binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated + binding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1) + binding.seedReminderView.setProgress(80, false) + binding.seedReminderView.delegate = this@HomeActivity } else { - binding.seedReminderStub.isVisible = false + binding.seedReminderView.isVisible = false } + setupHeaderImage() // Set up recycler view + binding.globalSearchInputLayout.listener = this homeAdapter.setHasStableIds(true) homeAdapter.glide = glide binding.recyclerView.adapter = homeAdapter + binding.globalSearchRecycler.adapter = globalSearchAdapter // Set up empty state view binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() } IP2Country.configureIfNeeded(this@HomeActivity) @@ -129,7 +183,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis binding.newConversationButtonSet.delegate = this // Observe blocked contacts changed events val broadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { binding.recyclerView.adapter!!.notifyDataSetChanged() } @@ -161,10 +214,85 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis JobQueue.shared.resumePendingJobs() } } + // monitor the global search VM query + launch { + binding.globalSearchInputLayout.query + .onEach(globalSearchViewModel::postQuery) + .collect() + } + // Get group results and display them + launch { + globalSearchViewModel.result.collect { result -> + val currentUserPublicKey = publicKey + val contactAndGroupList = result.contacts.map { GlobalSearchAdapter.Model.Contact(it) } + + result.threads.map { GlobalSearchAdapter.Model.GroupConversation(it) } + + val contactResults = contactAndGroupList.toMutableList() + + if (contactResults.isEmpty()) { + contactResults.add(GlobalSearchAdapter.Model.SavedMessages(currentUserPublicKey)) + } + + val userIndex = contactResults.indexOfFirst { it is GlobalSearchAdapter.Model.Contact && it.contact.sessionID == currentUserPublicKey } + if (userIndex >= 0) { + contactResults[userIndex] = GlobalSearchAdapter.Model.SavedMessages(currentUserPublicKey) + } + + if (contactResults.isNotEmpty()) { + contactResults.add(0, GlobalSearchAdapter.Model.Header(R.string.global_search_contacts_groups)) + } + + val unreadThreadMap = result.messages + .groupBy { it.threadId }.keys + .map { it to mmsSmsDatabase.getUnreadCount(it) } + .toMap() + + val messageResults: MutableList = result.messages + .map { messageResult -> + GlobalSearchAdapter.Model.Message( + messageResult, + unreadThreadMap[messageResult.threadId] ?: 0 + ) + }.toMutableList() + + if (messageResults.isNotEmpty()) { + messageResults.add(0, GlobalSearchAdapter.Model.Header(R.string.global_search_messages)) + } + + val newData = contactResults + messageResults + + globalSearchAdapter.setNewData(result.query, newData) + } + } } EventBus.getDefault().register(this@HomeActivity) } + private fun setupHeaderImage() { + val isDayUiMode = UiModeUtilities.isDayUiMode(this) + val headerTint = if (isDayUiMode) R.color.black else R.color.accent + binding.sessionHeaderImage.setColorFilter(getColor(headerTint)) + } + + override fun onInputFocusChanged(hasFocus: Boolean) { + if (hasFocus) { + setSearchShown(true) + } else { + setSearchShown(!binding.globalSearchInputLayout.query.value.isNullOrEmpty()) + } + } + + private fun setSearchShown(isShown: Boolean) { + binding.searchToolbar.isVisible = isShown + binding.sessionToolbar.isVisible = !isShown + binding.recyclerView.isVisible = !isShown + binding.emptyStateContainer.isVisible = (binding.recyclerView.adapter as HomeAdapter).itemCount == 0 && binding.recyclerView.isVisible + binding.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown + binding.gradientView.isVisible = !isShown + binding.globalSearchRecycler.isVisible = isShown + binding.newConversationButtonSet.isVisible = !isShown + } + override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { return HomeLoader(this@HomeActivity) } @@ -187,7 +315,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis binding.profileButton.update() val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) if (hasViewedSeed) { - binding.seedReminderStub.isVisible = false + binding.seedReminderView.isVisible = false } if (TextSecurePreferences.getConfigurationMessageSynced(this)) { lifecycleScope.launch(Dispatchers.IO) { @@ -221,7 +349,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis // region Updating private fun updateEmptyState() { val threadCount = (binding.recyclerView.adapter as HomeAdapter).itemCount - binding.emptyStateContainer.isVisible = threadCount == 0 + binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible } @Subscribe(threadMode = ThreadMode.MAIN) @@ -240,6 +368,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis // endregion // region Interaction + override fun onBackPressed() { + if (binding.globalSearchRecycler.isVisible) { + binding.globalSearchInputLayout.clearSearch(true) + return + } + super.onBackPressed() + } + override fun handleSeedReminderViewContinueButtonTapped() { val intent = Intent(this, SeedActivity::class.java) show(intent) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt new file mode 100644 index 000000000..554fb2e11 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt @@ -0,0 +1,129 @@ +package org.thoughtcrime.securesms.home.search + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import network.loki.messenger.R +import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding +import network.loki.messenger.databinding.ViewGlobalSearchResultBinding +import org.session.libsession.utilities.GroupRecord +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.search.model.MessageResult +import java.security.InvalidParameterException +import org.session.libsession.messaging.contacts.Contact as ContactModel + +class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerView.Adapter() { + + companion object { + const val HEADER_VIEW_TYPE = 0 + const val CONTENT_VIEW_TYPE = 1 + } + + private var data: List = listOf() + private var query: String? = null + + fun setNewData(query: String, newData: List) { + val diffResult = DiffUtil.calculateDiff(GlobalSearchDiff(this.query, query, data, newData)) + this.query = query + data = newData + diffResult.dispatchUpdatesTo(this) + } + + override fun getItemViewType(position: Int): Int = + if (data[position] is Model.Header) HEADER_VIEW_TYPE else CONTENT_VIEW_TYPE + + override fun getItemCount(): Int = data.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + if (viewType == HEADER_VIEW_TYPE) { + HeaderView( + LayoutInflater.from(parent.context) + .inflate(R.layout.view_global_search_header, parent, false) + ) + } else { + ContentView( + LayoutInflater.from(parent.context) + .inflate(R.layout.view_global_search_result, parent, false) + , modelCallback) + } + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: MutableList + ) { + val newUpdateQuery: String? = payloads.firstOrNull { it is String } as String? + if (newUpdateQuery != null && holder is ContentView) { + holder.bindPayload(newUpdateQuery, data[position]) + return + } + if (holder is HeaderView) { + holder.bind(data[position] as Model.Header) + } else if (holder is ContentView) { + holder.bind(query.orEmpty(), data[position]) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + onBindViewHolder(holder,position, mutableListOf()) + } + + class HeaderView(view: View) : RecyclerView.ViewHolder(view) { + + val binding = ViewGlobalSearchHeaderBinding.bind(view) + + fun bind(header: Model.Header) { + binding.searchHeader.setText(header.title) + } + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + if (holder is ContentView) { + holder.binding.searchResultProfilePicture.recycle() + } + } + + class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) { + + val binding = ViewGlobalSearchResultBinding.bind(view).apply { + searchResultProfilePicture.glide = GlideApp.with(root) + } + + fun bindPayload(newQuery: String, model: Model) { + bindQuery(newQuery, model) + } + + fun bind(query: String, model: Model) { + binding.searchResultProfilePicture.recycle() + when (model) { + is Model.GroupConversation -> bindModel(query, model) + is Model.Contact -> bindModel(query, model) + is Model.Message -> bindModel(query, model) + is Model.SavedMessages -> bindModel(model) + is Model.Header -> throw InvalidParameterException("Can't display Model.Header as ContentView") + } + binding.root.setOnClickListener { modelCallback(model) } + } + + } + + data class MessageModel( + val threadRecipient: Recipient, + val messageRecipient: Recipient, + val messageSnippet: String + ) + + sealed class Model { + data class Header(@StringRes val title: Int) : Model() + data class SavedMessages(val currentUserPublicKey: String): Model() + data class Contact(val contact: ContactModel) : Model() + data class GroupConversation(val groupRecord: GroupRecord) : Model() + data class Message(val messageResult: MessageResult, val unread: Int) : Model() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt new file mode 100644 index 000000000..2181b7f83 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt @@ -0,0 +1,160 @@ +package org.thoughtcrime.securesms.home.search + +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.util.TypedValue +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import network.loki.messenger.R +import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView +import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation +import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message +import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages +import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.SearchUtil +import java.util.Locale +import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel + + +class GlobalSearchDiff( + private val oldQuery: String?, + private val newQuery: String?, + private val oldData: List, + private val newData: List +) : DiffUtil.Callback() { + override fun getOldListSize(): Int = oldData.size + override fun getNewListSize(): Int = newData.size + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + oldData[oldItemPosition] == newData[newItemPosition] + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + oldQuery == newQuery && oldData[oldItemPosition] == newData[newItemPosition] + + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? = + if (oldQuery != newQuery) newQuery + else null +} + +private val BoldStyleFactory = { StyleSpan(Typeface.BOLD) } + +fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) { + when (model) { + is ContactModel -> { + binding.searchResultTitle.text = getHighlight( + query, + model.contact.getSearchName() + ) + } + is Message -> { + val textSpannable = SpannableStringBuilder() + if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) { + // group chat, bind + val text = "${model.messageResult.messageRecipient.getSearchName()}: " + textSpannable.append(text) + } + textSpannable.append(getHighlight( + query, + model.messageResult.bodySnippet + )) + binding.searchResultSubtitle.text = textSpannable + binding.searchResultSubtitle.isVisible = true + binding.searchResultTitle.text = model.messageResult.conversationRecipient.toShortString() + } + is GroupConversation -> { + binding.searchResultTitle.text = getHighlight( + query, + model.groupRecord.title + ) + + val membersString = model.groupRecord.members.joinToString { address -> + val recipient = Recipient.from(binding.root.context, address, false) + recipient.name ?: "${address.serialize().take(4)}...${address.serialize().takeLast(4)}" + } + binding.searchResultSubtitle.text = getHighlight(query, membersString) + } + } +} + +private fun getHighlight(query: String?, toSearch: String): Spannable? { + return SearchUtil.getHighlightedSpan(Locale.getDefault(), BoldStyleFactory, toSearch, query) +} + +fun ContentView.bindModel(query: String?, model: GroupConversation) { + binding.searchResultProfilePicture.isVisible = true + binding.searchResultSavedMessages.isVisible = false + binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup + binding.searchResultTimestamp.isVisible = false + val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false) + binding.searchResultProfilePicture.update(threadRecipient) + val nameString = model.groupRecord.title + binding.searchResultTitle.text = getHighlight(query, nameString) + + val groupRecipients = model.groupRecord.members.map { Recipient.from(binding.root.context, it, false) } + + val membersString = groupRecipients.joinToString { + val address = it.address.serialize() + it.name ?: "${address.take(4)}...${address.takeLast(4)}" + } + if (model.groupRecord.isClosedGroup) { + binding.searchResultSubtitle.text = getHighlight(query, membersString) + } +} + +fun ContentView.bindModel(query: String?, model: ContactModel) { + binding.searchResultProfilePicture.isVisible = true + binding.searchResultSavedMessages.isVisible = false + binding.searchResultSubtitle.isVisible = false + binding.searchResultTimestamp.isVisible = false + binding.searchResultSubtitle.text = null + val recipient = + Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false) + binding.searchResultProfilePicture.update(recipient) + val nameString = model.contact.getSearchName() + binding.searchResultTitle.text = getHighlight(query, nameString) +} + +fun ContentView.bindModel(model: SavedMessages) { + binding.searchResultSubtitle.isVisible = false + binding.searchResultTimestamp.isVisible = false + binding.searchResultTitle.setText(R.string.note_to_self) + binding.searchResultProfilePicture.isVisible = false + binding.searchResultSavedMessages.isVisible = true +} + +fun ContentView.bindModel(query: String?, model: Message) { + binding.searchResultProfilePicture.isVisible = true + binding.searchResultSavedMessages.isVisible = false + binding.searchResultTimestamp.isVisible = true +// val hasUnreads = model.unread > 0 +// binding.unreadCountIndicator.isVisible = hasUnreads +// if (hasUnreads) { +// binding.unreadCountTextView.text = model.unread.toString() +// } + binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.receivedTimestampMs) + binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient) + val textSpannable = SpannableStringBuilder() + if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) { + // group chat, bind + val text = "${model.messageResult.messageRecipient.getSearchName()}: " + textSpannable.append(text) + } + textSpannable.append(getHighlight( + query, + model.messageResult.bodySnippet + )) + binding.searchResultSubtitle.text = textSpannable + binding.searchResultTitle.text = model.messageResult.conversationRecipient.toShortString() + binding.searchResultSubtitle.isVisible = true +} + +fun Recipient.getSearchName(): String = name ?: address.serialize().let { address -> "${address.take(4)}...${address.takeLast(4)}" } + +fun Contact.getSearchName(): String = + if (nickname.isNullOrEmpty()) name ?: "${sessionID.take(4)}...${sessionID.takeLast(4)}" + else "${name ?: "${sessionID.take(4)}...${sessionID.takeLast(4)}"} ($nickname)" \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt new file mode 100644 index 000000000..411ae0956 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt @@ -0,0 +1,88 @@ +package org.thoughtcrime.securesms.home.search + +import android.content.Context +import android.text.Editable +import android.text.TextWatcher +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.LinearLayout +import android.widget.TextView +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import network.loki.messenger.databinding.ViewGlobalSearchInputBinding + +class GlobalSearchInputLayout @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : LinearLayout(context, attrs), + View.OnFocusChangeListener, + View.OnClickListener, + TextWatcher, TextView.OnEditorActionListener { + + var binding: ViewGlobalSearchInputBinding = ViewGlobalSearchInputBinding.inflate(LayoutInflater.from(context), this, true) + + var listener: GlobalSearchInputLayoutListener? = null + + private val _query = MutableStateFlow(null) + val query: StateFlow = _query + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + binding.searchInput.onFocusChangeListener = this + binding.searchInput.addTextChangedListener(this) + binding.searchInput.setOnEditorActionListener(this) + binding.searchCancel.setOnClickListener(this) + binding.searchClear.setOnClickListener(this) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + } + + override fun onFocusChange(v: View?, hasFocus: Boolean) { + if (v === binding.searchInput) { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(windowToken, 0) + listener?.onInputFocusChanged(hasFocus) + } + } + + override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { + if (v === binding.searchInput && actionId == EditorInfo.IME_ACTION_SEARCH) { + binding.searchInput.clearFocus() + return true + } + return false + } + + override fun onClick(v: View?) { + if (v === binding.searchCancel) { + clearSearch(true) + } else if (v === binding.searchClear) { + clearSearch(false) + } + } + + fun clearSearch(clearFocus: Boolean) { + binding.searchInput.text = null + if (clearFocus) { + binding.searchInput.clearFocus() + } + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable?) { + _query.value = s?.toString() + } + + interface GlobalSearchInputLayoutListener { + fun onInputFocusChanged(hasFocus: Boolean) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt new file mode 100644 index 000000000..c85ffa874 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.home.search + +import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.utilities.GroupRecord +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.search.model.MessageResult +import org.thoughtcrime.securesms.search.model.SearchResult + +data class GlobalSearchResult( + val query: String, + val contacts: List, + val threads: List, + val messages: List +) { + + val isEmpty: Boolean + get() = contacts.isEmpty() && threads.isEmpty() && messages.isEmpty() + + companion object { + + val EMPTY = GlobalSearchResult("", emptyList(), emptyList(), emptyList()) + const val SEARCH_LIMIT = 5 + + fun from(searchResult: SearchResult): GlobalSearchResult { + val query = searchResult.query + val contactList = searchResult.contacts.toList() + val threads = searchResult.conversations.toList() + val messages = searchResult.messages.toList() + searchResult.close() + return GlobalSearchResult(query, contactList, threads, messages) + } + + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt new file mode 100644 index 000000000..8908554b0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt @@ -0,0 +1,69 @@ +package org.thoughtcrime.securesms.home.search + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.plus +import org.session.libsignal.utilities.SettableFuture +import org.thoughtcrime.securesms.search.SearchRepository +import org.thoughtcrime.securesms.search.model.SearchResult +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@HiltViewModel +class GlobalSearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() { + + private val executor = viewModelScope + SupervisorJob() + + private val _result: MutableStateFlow = + MutableStateFlow(GlobalSearchResult.EMPTY) + + val result: StateFlow = _result + + private val _queryText: MutableStateFlow = MutableStateFlow("") + + fun postQuery(charSequence: CharSequence?) { + charSequence ?: return + _queryText.value = charSequence + } + + init { + // + _queryText + .buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST) + .mapLatest { query -> + if (query.trim().length < 2) { + SearchResult.EMPTY + } else { + // user input delay here in case we get a new query within a few hundred ms + // this coroutine will be cancelled and expensive query will not be run if typing quickly + // first query of 2 characters will be instant however + delay(300) + val settableFuture = SettableFuture() + searchRepository.query(query.toString(), settableFuture::set) + try { + // search repository doesn't play nicely with suspend functions (yet) + settableFuture.get(10_000, TimeUnit.MILLISECONDS) + } catch (e: Exception) { + SearchResult.EMPTY + } + } + } + .onEach { result -> + // update the latest _result value + _result.value = GlobalSearchResult.from(result) + } + .launchIn(executor) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java index 0fe41d2a0..9e3db73bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java @@ -105,7 +105,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem onMediaSelectionChanged(new ArrayList<>(viewModel.getSelectedMedia().getValue())); } - viewModel.getMediaInBucket(requireContext(), bucketId).observe(this, adapter::setMedia); + viewModel.getMediaInBucket(requireContext(), bucketId).observe(getViewLifecycleOwner(), adapter::setMedia); initMediaObserver(viewModel); } @@ -178,7 +178,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem } private void initMediaObserver(@NonNull MediaSendViewModel viewModel) { - viewModel.getCountButtonState().observe(this, media -> { + viewModel.getCountButtonState().observe(getViewLifecycleOwner(), media -> { requireActivity().invalidateOptionsMenu(); }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt index 22a3e037b..cac1bfb9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt @@ -60,7 +60,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor val closedGroupPoller = ClosedGroupPollerV2() // Intentionally don't use shared val storage = MessagingModuleConfiguration.shared.storage val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys() - allGroupPublicKeys.forEach { closedGroupPoller.poll(it) } + allGroupPublicKeys.iterator().forEach { closedGroupPoller.poll(it) } // Open Groups val threadDB = DatabaseComponent.get(context).lokiThreadDatabase() diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt index ac6039063..48a649dee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt @@ -57,7 +57,7 @@ object LokiPushNotificationManager { // Unsubscribe from all closed groups val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys() val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - allClosedGroupPublicKeys.forEach { closedGroup -> + allClosedGroupPublicKeys.iterator().forEach { closedGroup -> performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey) } } @@ -87,7 +87,7 @@ object LokiPushNotificationManager { } // Subscribe to all closed groups val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys() - allClosedGroupPublicKeys.forEach { closedGroup -> + allClosedGroupPublicKeys.iterator().forEach { closedGroup -> performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java index 6b097a0d5..7bf967237 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java @@ -54,9 +54,9 @@ public abstract class CorrectedPreferenceFragment extends PreferenceFragmentComp } @Override + @SuppressLint("RestrictedApi") protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { return new PreferenceGroupAdapter(preferenceScreen) { - @SuppressLint("RestrictedApi") @Override public void onBindViewHolder(PreferenceViewHolder holder, int position) { super.onBindViewHolder(holder, position); diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index fb9d13f0f..d5c7747e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -34,6 +34,7 @@ import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection +import org.thoughtcrime.securesms.home.PathActivity import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.permissions.Permissions @@ -42,7 +43,9 @@ import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.UiModeUtilities +import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.util.show import java.io.File import java.security.SecureRandom import java.util.Date @@ -84,6 +87,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { publicKeyTextView.text = hexEncodedPublicKey copyButton.setOnClickListener { copyPublicKey() } shareButton.setOnClickListener { sharePublicKey() } + pathButton.setOnClickListener { showPath() } + pathContainer.disableClipping() privacyButton.setOnClickListener { showPrivacySettings() } notificationsButton.setOnClickListener { showNotificationSettings() } chatsButton.setOnClickListener { showChatSettings() } @@ -303,6 +308,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } } + private fun showPath() { + val intent = Intent(this, PathActivity::class.java) + show(intent) + } + private fun showSurvey() { try { val url = "https://getsession.org/survey" diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchModule.kt b/app/src/main/java/org/thoughtcrime/securesms/search/SearchModule.kt new file mode 100644 index 000000000..7ee247fdb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchModule.kt @@ -0,0 +1,33 @@ +package org.thoughtcrime.securesms.search + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.scopes.ActivityScoped +import dagger.hilt.android.scopes.ViewModelScoped +import org.session.libsession.utilities.concurrent.SignalExecutors +import org.thoughtcrime.securesms.contacts.ContactAccessor +import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.SearchDatabase +import org.thoughtcrime.securesms.database.SessionContactDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase + +@Module +@InstallIn(ViewModelComponent::class) +object SearchModule { + + @Provides + @ViewModelScoped + fun provideSearchRepository(@ApplicationContext context: Context, + searchDatabase: SearchDatabase, + threadDatabase: ThreadDatabase, + groupDatabase: GroupDatabase, + contactDatabase: SessionContactDatabase) = + SearchRepository(context, searchDatabase, threadDatabase, groupDatabase, contactDatabase, ContactAccessor.getInstance(), SignalExecutors.SERIAL) + + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java index df4cb1d17..a33f4dd11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java @@ -3,29 +3,39 @@ package org.thoughtcrime.securesms.search; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; -import androidx.annotation.NonNull; +import android.database.MergeCursor; import android.text.TextUtils; +import androidx.annotation.NonNull; + import com.annimon.stream.Stream; -import org.thoughtcrime.securesms.contacts.ContactAccessor; +import org.session.libsession.messaging.contacts.Contact; import org.session.libsession.utilities.Address; +import org.session.libsession.utilities.GroupRecord; +import org.session.libsession.utilities.TextSecurePreferences; +import org.session.libsession.utilities.recipients.Recipient; +import org.session.libsignal.utilities.Log; +import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.database.CursorList; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SearchDatabase; +import org.thoughtcrime.securesms.database.SessionContactDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.model.ThreadRecord; -import org.session.libsignal.utilities.Log; -import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.search.model.MessageResult; import org.thoughtcrime.securesms.search.model.SearchResult; import org.thoughtcrime.securesms.util.Stopwatch; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; +import kotlin.Pair; + /** * Manages data retrieval for search. */ @@ -50,21 +60,27 @@ public class SearchRepository { } } - private final Context context; - private final SearchDatabase searchDatabase; - private final ThreadDatabase threadDatabase; - private final ContactAccessor contactAccessor; - private final Executor executor; + private final Context context; + private final SearchDatabase searchDatabase; + private final ThreadDatabase threadDatabase; + private final GroupDatabase groupDatabase; + private final SessionContactDatabase contactDatabase; + private final ContactAccessor contactAccessor; + private final Executor executor; public SearchRepository(@NonNull Context context, @NonNull SearchDatabase searchDatabase, @NonNull ThreadDatabase threadDatabase, + @NonNull GroupDatabase groupDatabase, + @NonNull SessionContactDatabase contactDatabase, @NonNull ContactAccessor contactAccessor, @NonNull Executor executor) { this.context = context.getApplicationContext(); this.searchDatabase = searchDatabase; this.threadDatabase = threadDatabase; + this.groupDatabase = groupDatabase; + this.contactDatabase = contactDatabase; this.contactAccessor = contactAccessor; this.executor = executor; } @@ -81,10 +97,10 @@ public class SearchRepository { String cleanQuery = sanitizeQuery(query); timer.split("clean"); - CursorList contacts = queryContacts(cleanQuery); + Pair, List> contacts = queryContacts(cleanQuery); timer.split("contacts"); - CursorList conversations = queryConversations(cleanQuery); + CursorList conversations = queryConversations(cleanQuery, contacts.getSecond()); timer.split("conversations"); CursorList messages = queryMessages(cleanQuery); @@ -92,7 +108,7 @@ public class SearchRepository { timer.stop(TAG); - callback.onResult(new SearchResult(cleanQuery, contacts, conversations, messages)); + callback.onResult(new SearchResult(cleanQuery, contacts.getFirst(), conversations, messages)); }); } @@ -111,28 +127,62 @@ public class SearchRepository { }); } - private CursorList queryContacts(String query) { - return CursorList.emptyList(); - /* Loki - We don't need contacts permission - if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) { - return CursorList.emptyList(); + private Pair, List> queryContacts(String query) { + + Cursor contacts = contactDatabase.queryContactsByName(query); + List
contactList = new ArrayList<>(); + List contactStrings = new ArrayList<>(); + + while (contacts.moveToNext()) { + try { + Contact contact = contactDatabase.contactFromCursor(contacts); + String contactSessionId = contact.getSessionID(); + Address address = Address.fromSerialized(contactSessionId); + contactList.add(address); + contactStrings.add(contactSessionId); + } catch (Exception e) { + Log.e("Loki", "Error building Contact from cursor in query", e); + } } - Cursor textSecureContacts = contactsDatabase.queryTextSecureContacts(query); - Cursor systemContacts = contactsDatabase.querySystemContacts(query); - MergeCursor contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts }); + contacts.close(); + + Cursor addressThreads = threadDatabase.searchConversationAddresses(query); + Cursor individualRecipients = threadDatabase.getFilteredConversationList(contactList); + if (individualRecipients == null && addressThreads == null) { + return new Pair<>(CursorList.emptyList(),contactStrings); + } + MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients}); + + return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings); - return new CursorList<>(contacts, new RecipientModelBuilder(context)); - */ } - private CursorList queryConversations(@NonNull String query) { + private CursorList queryConversations(@NonNull String query, List matchingAddresses) { List numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query); - List
addresses = Stream.of(numbers).map(number -> Address.fromExternal(context, number)).toList(); + String localUserNumber = TextSecurePreferences.getLocalNumber(context); + if (localUserNumber != null) { + matchingAddresses.remove(localUserNumber); + } + Set
addresses = new HashSet<>(Stream.of(numbers).map(number -> Address.fromExternal(context, number)).toList()); - Cursor conversations = threadDatabase.getFilteredConversationList(addresses); - return conversations != null ? new CursorList<>(conversations, new ThreadModelBuilder(threadDatabase)) - : CursorList.emptyList(); + Cursor membersGroupList = groupDatabase.getGroupsFilteredByMembers(matchingAddresses); + if (membersGroupList != null) { + GroupDatabase.Reader reader = new GroupDatabase.Reader(membersGroupList); + while (membersGroupList.moveToNext()) { + GroupRecord record = reader.getCurrent(); + if (record == null) continue; + + addresses.add(Address.fromSerialized(record.getEncodedId())); + } + membersGroupList.close(); + } + + + Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses)); + + return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase)) + : CursorList.emptyList(); } private CursorList queryMessages(@NonNull String query) { @@ -169,6 +219,28 @@ public class SearchRepository { return out.toString(); } + private static class ContactModelBuilder implements CursorList.ModelBuilder { + + private final SessionContactDatabase contactDb; + private final ThreadDatabase threadDb; + + public ContactModelBuilder(SessionContactDatabase contactDb, ThreadDatabase threadDb) { + this.contactDb = contactDb; + this.threadDb = threadDb; + } + + @Override + public Contact build(@NonNull Cursor cursor) { + ThreadRecord threadRecord = threadDb.readerFor(cursor).getCurrent(); + Contact contact = contactDb.getContactWithSessionID(threadRecord.getRecipient().getAddress().serialize()); + if (contact == null) { + contact = new Contact(threadRecord.getRecipient().getAddress().serialize()); + contact.setThreadID(threadRecord.getThreadId()); + } + return contact; + } + } + private static class RecipientModelBuilder implements CursorList.ModelBuilder { private final Context context; @@ -184,6 +256,22 @@ public class SearchRepository { } } + private static class GroupModelBuilder implements CursorList.ModelBuilder { + private final ThreadDatabase threadDatabase; + private final GroupDatabase groupDatabase; + + public GroupModelBuilder(ThreadDatabase threadDatabase, GroupDatabase groupDatabase) { + this.threadDatabase = threadDatabase; + this.groupDatabase = groupDatabase; + } + + @Override + public GroupRecord build(@NonNull Cursor cursor) { + ThreadRecord threadRecord = threadDatabase.readerFor(cursor).getCurrent(); + return groupDatabase.getGroup(threadRecord.getRecipient().getAddress().toGroupString()).get(); + } + } + private static class ThreadModelBuilder implements CursorList.ModelBuilder { private final ThreadDatabase threadDatabase; @@ -208,7 +296,7 @@ public class SearchRepository { @Override public MessageResult build(@NonNull Cursor cursor) { - Address conversationAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndex(SearchDatabase.CONVERSATION_ADDRESS))); + Address conversationAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.CONVERSATION_ADDRESS))); Address messageAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.MESSAGE_ADDRESS))); Recipient conversationRecipient = Recipient.from(context, conversationAddress, false); Recipient messageRecipient = Recipient.from(context, messageAddress, false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java b/app/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java index c1e40beb4..33c687c93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java +++ b/app/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java @@ -4,9 +4,10 @@ import android.database.ContentObserver; import androidx.annotation.NonNull; +import org.session.libsession.messaging.contacts.Contact; +import org.session.libsession.utilities.GroupRecord; import org.thoughtcrime.securesms.database.CursorList; import org.thoughtcrime.securesms.database.model.ThreadRecord; -import org.session.libsession.utilities.recipients.Recipient; import java.util.List; @@ -19,13 +20,13 @@ public class SearchResult { public static final SearchResult EMPTY = new SearchResult("", CursorList.emptyList(), CursorList.emptyList(), CursorList.emptyList()); private final String query; - private final CursorList contacts; - private final CursorList conversations; + private final CursorList contacts; + private final CursorList conversations; private final CursorList messages; public SearchResult(@NonNull String query, - @NonNull CursorList contacts, - @NonNull CursorList conversations, + @NonNull CursorList contacts, + @NonNull CursorList conversations, @NonNull CursorList messages) { this.query = query; @@ -34,11 +35,11 @@ public class SearchResult { this.messages = messages; } - public List getContacts() { + public List getContacts() { return contacts; } - public List getConversations() { + public List getConversations() { return conversations; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt index 3f4867839..eaaf06f45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -229,7 +229,7 @@ object BackupUtil { @JvmOverloads fun deleteAllBackupFiles(context: Context, except: Collection? = null) { val db = DatabaseComponent.get(context).lokiBackupFilesDatabase() - db.getBackupFiles().forEach { record -> + db.getBackupFiles().iterator().forEach { record -> if (except != null && except.contains(record)) return@forEach // Try to delete the related file. The operation may fail in many cases diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java index 7860e4624..874440f5d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java @@ -121,14 +121,12 @@ public class DateUtils extends android.text.format.DateUtils { * e.g. 2020-09-04T19:17:51Z * https://www.iso.org/iso-8601-date-and-time-format.html * - * Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences. - * * @return The timestamp if able to be parsed, otherwise -1. */ @SuppressLint("ObsoleteSdkInt") public static long parseIso8601(@Nullable String date) { SimpleDateFormat format; - if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) { + if (Build.VERSION.SDK_INT >= 24) { format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); } else { format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt b/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt index 4ff45e8f0..479a54faf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt @@ -116,8 +116,8 @@ class IP2Country private constructor(private val context: Context) { private fun populateCacheIfNeeded() { ThreadUtils.queue { - OnionRequestAPI.paths.forEach { path -> - path.forEach { snode -> + OnionRequestAPI.paths.iterator().forEach { path -> + path.iterator().forEach { snode -> cacheCountryForIP(snode.ip) // Preload if needed } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SearchUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SearchUtil.java index 8935780b1..cf046b9a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SearchUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SearchUtil.java @@ -1,12 +1,13 @@ package org.thoughtcrime.securesms.util; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.CharacterStyle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.annimon.stream.Stream; import org.session.libsignal.utilities.Pair; diff --git a/app/src/main/res/drawable/ic_outline_bookmark_border_24.xml b/app/src/main/res/drawable/ic_outline_bookmark_border_24.xml new file mode 100644 index 000000000..0cb95b770 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_bookmark_border_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_session.xml b/app/src/main/res/drawable/ic_session.xml new file mode 100644 index 000000000..040347af1 --- /dev/null +++ b/app/src/main/res/drawable/ic_session.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/search_background.xml b/app/src/main/res/drawable/search_background.xml new file mode 100644 index 000000000..a2090ca6b --- /dev/null +++ b/app/src/main/res/drawable/search_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/roboto_medium.ttf b/app/src/main/res/font/roboto_medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e89b0b79a2910f0ac309a1845a9f733bcb568792 GIT binary patch literal 168644 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 yL< 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 - + + + android:layout_centerInParent="true" + android:layout_toStartOf="@+id/searchViewContainer" + android:layout_toEndOf="@+id/profileButton" + android:padding="@dimen/medium_spacing" + android:scaleType="centerInside" + android:src="@drawable/ic_session" + app:tint="@color/black" /> - + + + + + - @@ -100,6 +121,16 @@ android:layout_height="match_parent" android:background="@drawable/home_activity_gradient" /> + + + + + + + + + + + + android:layout_height="match_parent" + android:animateLayoutChanges="true"> diff --git a/app/src/main/res/layout/alert_view.xml b/app/src/main/res/layout/alert_view.xml index fe2ff895c..a3366c40c 100644 --- a/app/src/main/res/layout/alert_view.xml +++ b/app/src/main/res/layout/alert_view.xml @@ -1,6 +1,7 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto"> @@ -17,7 +18,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_error" - android:tint="@color/core_grey_60" + app:tint="@color/core_grey_60" android:visibility="gone" tools:visibility="visible" android:layout_gravity="center_vertical" diff --git a/app/src/main/res/layout/camera_fragment.xml b/app/src/main/res/layout/camera_fragment.xml index ccddcb26e..95cd0b379 100644 --- a/app/src/main/res/layout/camera_fragment.xml +++ b/app/src/main/res/layout/camera_fragment.xml @@ -1,6 +1,6 @@ - + app:tint="@android:color/white"/> diff --git a/app/src/main/res/layout/delivery_status_view.xml b/app/src/main/res/layout/delivery_status_view.xml index f38532a10..863055ce4 100644 --- a/app/src/main/res/layout/delivery_status_view.xml +++ b/app/src/main/res/layout/delivery_status_view.xml @@ -1,6 +1,7 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto"> \ No newline at end of file diff --git a/app/src/main/res/layout/emoji_display_item.xml b/app/src/main/res/layout/emoji_display_item.xml index 92c766ac6..cf8da830d 100644 --- a/app/src/main/res/layout/emoji_display_item.xml +++ b/app/src/main/res/layout/emoji_display_item.xml @@ -33,6 +33,6 @@ android:layout_height="7dp" android:layout_gravity="bottom|right|end" app:srcCompat="@drawable/triangle_bottom_right_corner" - android:tint="@color/core_grey_25"/> + app:tint="@color/core_grey_25"/> \ No newline at end of file diff --git a/app/src/main/res/layout/media_keyboard.xml b/app/src/main/res/layout/media_keyboard.xml index 76a628107..20ef09afe 100644 --- a/app/src/main/res/layout/media_keyboard.xml +++ b/app/src/main/res/layout/media_keyboard.xml @@ -18,7 +18,7 @@ android:layout_marginStart="6dp" android:padding="6dp" android:src="@drawable/ic_baseline_search_24" - android:tint="?media_keyboard_button_color" + app:tint="?media_keyboard_button_color" android:background="?selectableItemBackgroundBorderless" android:visibility="invisible" app:layout_constraintStart_toStartOf="parent" @@ -95,7 +95,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="12dp" android:scaleType="fitCenter" - android:tint="?media_keyboard_button_color" + app:tint="?media_keyboard_button_color" android:visibility="gone" android:background="?selectableItemBackground" app:srcCompat="@drawable/ic_baseline_add_24" diff --git a/app/src/main/res/layout/media_view_remove_button.xml b/app/src/main/res/layout/media_view_remove_button.xml index ffe87c4f8..735042693 100644 --- a/app/src/main/res/layout/media_view_remove_button.xml +++ b/app/src/main/res/layout/media_view_remove_button.xml @@ -1,10 +1,11 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/remove_image_button" + android:layout_width="@dimen/media_bubble_remove_button_size" + android:layout_height="@dimen/media_bubble_remove_button_size" + android:layout_gravity="top|end" + android:background="@drawable/circle_alpha" + android:src="@drawable/ic_close_white_18dp" + app:tint="#99FFFFFF" + android:visibility="gone" /> diff --git a/app/src/main/res/layout/mediapicker_folder_item.xml b/app/src/main/res/layout/mediapicker_folder_item.xml index b52ace6f4..a7b1547a7 100644 --- a/app/src/main/res/layout/mediapicker_folder_item.xml +++ b/app/src/main/res/layout/mediapicker_folder_item.xml @@ -1,9 +1,9 @@ - @@ -33,7 +33,7 @@ android:layout_width="20dp" android:layout_height="20dp" android:layout_marginEnd="6dp" - android:tint="@android:color/white" + app:tint="@android:color/white" android:src="@drawable/ic_baseline_folder_24"/> diff --git a/app/src/main/res/layout/mediasend_activity.xml b/app/src/main/res/layout/mediasend_activity.xml index bdfc0446c..fdf143313 100644 --- a/app/src/main/res/layout/mediasend_activity.xml +++ b/app/src/main/res/layout/mediasend_activity.xml @@ -1,7 +1,7 @@ - @@ -47,7 +47,7 @@ android:layout_height="20dp" android:layout_marginStart="2dp" android:src="@drawable/ic_arrow_right" - android:tint="@color/core_white"/> + app:tint="@color/core_white"/> @@ -60,7 +60,7 @@ android:layout_gravity="bottom|start" android:padding="12dp" android:src="@drawable/ic_camera_filled_24" - android:tint="@color/core_grey_60" + app:tint="@color/core_grey_60" android:background="@drawable/media_camera_button_background" android:elevation="4dp" android:visibility="gone" diff --git a/app/src/main/res/layout/mediasend_fragment.xml b/app/src/main/res/layout/mediasend_fragment.xml index 2093dec5a..87dd1c122 100644 --- a/app/src/main/res/layout/mediasend_fragment.xml +++ b/app/src/main/res/layout/mediasend_fragment.xml @@ -165,7 +165,7 @@ android:layout_width="36dp" android:layout_height="36dp" android:src="@drawable/ic_baseline_clear_24" - android:tint="@android:color/white"/> + app:tint="@android:color/white"/> diff --git a/app/src/main/res/layout/quote_view.xml b/app/src/main/res/layout/quote_view.xml index 50b270b50..94ee633ec 100644 --- a/app/src/main/res/layout/quote_view.xml +++ b/app/src/main/res/layout/quote_view.xml @@ -133,7 +133,7 @@ android:layout_height="16dp" android:layout_marginStart="11dp" android:layout_marginTop="8dp" - android:tint="@color/core_blue" + app:tint="@color/core_blue" android:scaleType="fitXY" app:srcCompat="@drawable/triangle_right" /> @@ -157,7 +157,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:src="@drawable/ic_broken_link" - android:tint="@color/text"/> + app:tint="@color/text"/> - diff --git a/app/src/main/res/layout/thumbnail_view.xml b/app/src/main/res/layout/thumbnail_view.xml index eaa98cce6..eca3485df 100644 --- a/app/src/main/res/layout/thumbnail_view.xml +++ b/app/src/main/res/layout/thumbnail_view.xml @@ -45,7 +45,7 @@ android:layout_height="24dp" android:layout_marginStart="17dp" android:layout_marginTop="12dp" - android:tint="@color/core_blue" + app:tint="@color/core_blue" android:scaleType="fitXY" app:srcCompat="@drawable/triangle_right" /> diff --git a/app/src/main/res/layout/transfer_controls_view.xml b/app/src/main/res/layout/transfer_controls_view.xml index 7a54d547b..b76c2bd30 100644 --- a/app/src/main/res/layout/transfer_controls_view.xml +++ b/app/src/main/res/layout/transfer_controls_view.xml @@ -35,7 +35,7 @@ + android:textSize="@dimen/very_small_font_size" + android:textStyle="bold" /> diff --git a/app/src/main/res/layout/view_global_search_header.xml b/app/src/main/res/layout/view_global_search_header.xml new file mode 100644 index 000000000..c04f289c3 --- /dev/null +++ b/app/src/main/res/layout/view_global_search_header.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_global_search_input.xml b/app/src/main/res/layout/view_global_search_input.xml new file mode 100644 index 000000000..058fa6119 --- /dev/null +++ b/app/src/main/res/layout/view_global_search_input.xml @@ -0,0 +1,58 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_global_search_result.xml b/app/src/main/res/layout/view_global_search_result.xml new file mode 100644 index 000000000..b784ca559 --- /dev/null +++ b/app/src/main/res/layout/view_global_search_result.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_home.xml b/app/src/main/res/menu/menu_home.xml new file mode 100644 index 000000000..6b204492b --- /dev/null +++ b/app/src/main/res/menu/menu_home.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index b98c41484..6aa23d4db 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -238,7 +238,7 @@ on viallinen! Katoavat viestit poistettu käytöstä Katoavien viestien ajaksi asetettu %s %s otti kuvakaappauksen. - Media tallennettu toimesta. + Media tallennettu toimesta %s. Turvanumero vaihtunut Sinun ja yhteystiedon %s turvanumero on vaihtunut. Merkitsit varmennetuksi. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 9e48982c9..4e92ea71b 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -238,7 +238,7 @@ on viallinen! Katoavat viestit poistettu käytöstä Katoavien viestien ajaksi asetettu %s %s otti kuvakaappauksen. - Media tallennettu toimesta. + Media tallennettu toimesta %s. Turvanumero vaihtunut Sinun ja yhteystiedon %s turvanumero on vaihtunut. Merkitsit varmennetuksi. diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml index 8e4d97b84..77e9d7a21 100644 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -729,5 +729,4 @@ ये संदेश मिटा दिया है स्वयं के लिये मिटाये सभी के लिए संदेश मिटायें - स्वयमेव और प्राप्तिकर्ता के लिये मिटाये diff --git a/app/src/main/res/values-notnight-v21/themes.xml b/app/src/main/res/values-notnight-v21/themes.xml index 53262d359..abbf3f871 100644 --- a/app/src/main/res/values-notnight-v21/themes.xml +++ b/app/src/main/res/values-notnight-v21/themes.xml @@ -5,6 +5,10 @@ ?android:navigationBarColor @color/gray50 + #F2F2F2 + #413F40 + #767676 + #00FFFFFF #FFFFFFFF diff --git a/app/src/main/res/values-sq-rAL/strings.xml b/app/src/main/res/values-sq-rAL/strings.xml index d02c676b7..cbdb24eae 100644 --- a/app/src/main/res/values-sq-rAL/strings.xml +++ b/app/src/main/res/values-sq-rAL/strings.xml @@ -237,7 +237,6 @@ %s është në Session! Zhdukja e mesazheve është e çaktivizuar Koha për zhdukje mesazhesh është vënë %s - Bëri nje screenshot Media u kursye me %s Numri i sigurisë ndryshoi Numri juaj i sigurisë me %s është ndryshuar. diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index b16b9029f..7375a81ba 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -8,7 +8,7 @@ #353535 #1B1B1B #0C0C0C - #171717 + #161616 #36383C #323232 #101011 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 32f3cfa12..947bd9f76 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -905,5 +905,7 @@ Pin Unpin Mark all as read + Contacts and Groups + Messages diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 1e78c0058..2dcc5a205 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -5,6 +5,9 @@