mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
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
This commit is contained in:
parent
561ce83aa4
commit
dd1da6b1a4
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="network.loki.messenger">
|
||||
package="network.loki.messenger.test">
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner"
|
||||
android:required="false" />
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -66,25 +66,18 @@ public class ContactAccessor {
|
||||
public List<String> getNumbersForThreadSearchFilter(Context context, String constraint) {
|
||||
LinkedList<String> 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;
|
||||
}
|
||||
|
@ -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<Address?>(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: 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<Cursor>) {
|
||||
@ -1296,7 +1312,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun resendMessage(messages: Set<MessageRecord>) {
|
||||
messages.forEach { messageRecord ->
|
||||
messages.iterator().forEach { messageRecord ->
|
||||
ResendMessageUtilities.resend(messageRecord)
|
||||
}
|
||||
endActionMode()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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<SearchResult>
|
||||
private val debouncer: Debouncer
|
||||
private val result: CloseableLiveData<SearchResult> = 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)
|
||||
}
|
||||
}
|
@ -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<GroupRecord> getGroup(Cursor cursor) {
|
||||
public Optional<GroupRecord> 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<String> 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<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
|
||||
List<Address> members = getCurrentMembers(groupId, false);
|
||||
List<Recipient> recipients = new LinkedList<>();
|
||||
|
@ -450,7 +450,7 @@ private inline fun <reified T> wrap(x: T): Array<T> {
|
||||
|
||||
private fun wrap(x: Map<String, String>): 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
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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<Address> 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;
|
||||
|
@ -200,7 +200,7 @@ class EnterChatURLFragment : Fragment() {
|
||||
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
|
||||
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)
|
||||
|
@ -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<Cursor> {
|
||||
class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
ConversationClickListener,
|
||||
SeedReminderViewDelegate,
|
||||
NewConversationButtonSetViewDelegate,
|
||||
LoaderManager.LoaderCallbacks<Cursor>,
|
||||
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<GlobalSearchViewModel>()
|
||||
|
||||
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<GlobalSearchAdapter.Model> = 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<Cursor> {
|
||||
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)
|
||||
|
@ -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<RecyclerView.ViewHolder>() {
|
||||
|
||||
companion object {
|
||||
const val HEADER_VIEW_TYPE = 0
|
||||
const val CONTENT_VIEW_TYPE = 1
|
||||
}
|
||||
|
||||
private var data: List<Model> = listOf()
|
||||
private var query: String? = null
|
||||
|
||||
fun setNewData(query: String, newData: List<Model>) {
|
||||
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<Any>
|
||||
) {
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
@ -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<GlobalSearchAdapter.Model>,
|
||||
private val newData: List<GlobalSearchAdapter.Model>
|
||||
) : 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)"
|
@ -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<CharSequence?>(null)
|
||||
val query: StateFlow<CharSequence?> = _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)
|
||||
}
|
||||
|
||||
}
|
@ -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<Contact>,
|
||||
val threads: List<GroupRecord>,
|
||||
val messages: List<MessageResult>
|
||||
) {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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<GlobalSearchResult> =
|
||||
MutableStateFlow(GlobalSearchResult.EMPTY)
|
||||
|
||||
val result: StateFlow<GlobalSearchResult> = _result
|
||||
|
||||
private val _queryText: MutableStateFlow<CharSequence> = 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<SearchResult>()
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
||||
|
||||
}
|
@ -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<Recipient> contacts = queryContacts(cleanQuery);
|
||||
Pair<CursorList<Contact>, List<String>> contacts = queryContacts(cleanQuery);
|
||||
timer.split("contacts");
|
||||
|
||||
CursorList<ThreadRecord> conversations = queryConversations(cleanQuery);
|
||||
CursorList<GroupRecord> conversations = queryConversations(cleanQuery, contacts.getSecond());
|
||||
timer.split("conversations");
|
||||
|
||||
CursorList<MessageResult> 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<Recipient> 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<CursorList<Contact>, List<String>> queryContacts(String query) {
|
||||
|
||||
Cursor contacts = contactDatabase.queryContactsByName(query);
|
||||
List<Address> contactList = new ArrayList<>();
|
||||
List<String> 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<ThreadRecord> queryConversations(@NonNull String query) {
|
||||
private CursorList<GroupRecord> queryConversations(@NonNull String query, List<String> matchingAddresses) {
|
||||
List<String> numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
|
||||
List<Address> addresses = Stream.of(numbers).map(number -> Address.fromExternal(context, number)).toList();
|
||||
String localUserNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
if (localUserNumber != null) {
|
||||
matchingAddresses.remove(localUserNumber);
|
||||
}
|
||||
Set<Address> 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<MessageResult> queryMessages(@NonNull String query) {
|
||||
@ -169,6 +219,28 @@ public class SearchRepository {
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static class ContactModelBuilder implements CursorList.ModelBuilder<Contact> {
|
||||
|
||||
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<Recipient> {
|
||||
|
||||
private final Context context;
|
||||
@ -184,6 +256,22 @@ public class SearchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private static class GroupModelBuilder implements CursorList.ModelBuilder<GroupRecord> {
|
||||
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<ThreadRecord> {
|
||||
|
||||
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);
|
||||
|
@ -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<Recipient> contacts;
|
||||
private final CursorList<ThreadRecord> conversations;
|
||||
private final CursorList<Contact> contacts;
|
||||
private final CursorList<GroupRecord> conversations;
|
||||
private final CursorList<MessageResult> messages;
|
||||
|
||||
public SearchResult(@NonNull String query,
|
||||
@NonNull CursorList<Recipient> contacts,
|
||||
@NonNull CursorList<ThreadRecord> conversations,
|
||||
@NonNull CursorList<Contact> contacts,
|
||||
@NonNull CursorList<GroupRecord> conversations,
|
||||
@NonNull CursorList<MessageResult> messages)
|
||||
{
|
||||
this.query = query;
|
||||
@ -34,11 +35,11 @@ public class SearchResult {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public List<Recipient> getContacts() {
|
||||
public List<Contact> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public List<ThreadRecord> getConversations() {
|
||||
public List<GroupRecord> getConversations() {
|
||||
return conversations;
|
||||
}
|
||||
|
||||
|
@ -229,7 +229,7 @@ object BackupUtil {
|
||||
@JvmOverloads
|
||||
fun deleteAllBackupFiles(context: Context, except: Collection<BackupFileRecord>? = 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
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
10
app/src/main/res/drawable/ic_outline_bookmark_border_24.xml
Normal file
10
app/src/main/res/drawable/ic_outline_bookmark_border_24.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,3L7,3c-1.1,0 -2,0.9 -2,2v16l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z"/>
|
||||
</vector>
|
27
app/src/main/res/drawable/ic_session.xml
Normal file
27
app/src/main/res/drawable/ic_session.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="766.2dp"
|
||||
android:height="102.3dp"
|
||||
android:viewportWidth="766.2"
|
||||
android:viewportHeight="102.3">
|
||||
<path
|
||||
android:pathData="M0.004,68.141h25.5c-0.1,6.5 8.3,13.3 28.3,13.7c18.7,0.1 33.1,-2.4 33.1,-11.1c0,-18.9 -84.7,3.2 -84.7,-38.7c0,-20 20.8,-32.1 51.6,-32.1c32,0 53.7,13.3 54,34L82.304,33.941c0.1,-6.7 -7.1,-13.6 -26.7,-13.7C38.704,20.141 26.404,22.841 26.404,31.041c0,18.3 84.5,-2 84.5,38.9c0,20.1 -22.1,32.3 -55.1,32.3C21.704,102.141 -0.396,88.641 0.004,68.141"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M125.723,2.247h95.2v20.4L149.723,22.647v18.1h69.9v19.1L149.723,59.847L149.723,79.647h71.2v20.4L125.723,100.047L125.723,2.247z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M234.365,68.141h25.5c-0.1,6.5 8.3,13.3 28.3,13.7c18.7,0.1 33.1,-2.4 33.1,-11.1c0,-18.9 -84.7,3.2 -84.7,-38.7c0,-20 20.8,-32.1 51.6,-32.1c32,0 53.7,13.3 54,34h-25.5c0.1,-6.7 -7.1,-13.6 -26.7,-13.7C273.065,20.141 260.765,22.841 260.765,31.041c0,18.3 84.5,-2 84.5,38.9c0,20.1 -22.1,32.3 -55.1,32.3C256.065,102.141 233.965,88.641 234.365,68.141"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M354.285,68.141L379.685,68.141c-0.1,6.5 8.3,13.3 28.3,13.7c18.7,0.1 33.1,-2.4 33.1,-11.1c0,-18.9 -84.7,3.2 -84.7,-38.7c0,-20 20.8,-32.1 51.6,-32.1c32,0 53.7,13.3 54,34h-25.5c0.1,-6.7 -7.1,-13.6 -26.7,-13.7C392.885,20.141 380.685,22.841 380.685,31.041c0,18.3 84.5,-2 84.5,38.9c0,20.1 -22.1,32.3 -55.1,32.3C375.985,102.141 353.885,88.641 354.285,68.141"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M480,2.3h24v97.6h-24V2.3z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M616.48,51.111c0,-19.9 -14.4,-30.7 -37.9,-30.7c-23.3,0 -37.7,10.9 -37.7,30.7s14.4,30.7 37.7,30.7C602.08,81.812 616.48,70.911 616.48,51.111M517.08,51.111c0,-30.7 23.6,-51.1 61.7,-51.1s61.7,20.4 61.7,51.1s-23.6,51.1 -61.9,51.1C540.68,102.212 517.08,81.812 517.08,51.111"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M653.525,2.247h20.1L742.125,64.447v-62.1h24v97.7h-20.3l-68.4,-62.1v62.1h-24L653.425,2.247z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
6
app/src/main/res/drawable/search_background.xml
Normal file
6
app/src/main/res/drawable/search_background.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?searchBackgroundColor"/>
|
||||
<corners android:radius="32dp"/>
|
||||
</shape>
|
BIN
app/src/main/res/font/roboto_medium.ttf
Normal file
BIN
app/src/main/res/font/roboto_medium.ttf
Normal file
Binary file not shown.
@ -21,6 +21,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/session_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_marginLeft="20dp"
|
||||
@ -34,44 +35,64 @@
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="9dp" />
|
||||
|
||||
<TextView
|
||||
<org.thoughtcrime.securesms.home.PathStatusView
|
||||
android:id="@+id/pathStatusView"
|
||||
android:layout_alignBottom="@+id/profileButton"
|
||||
android:layout_alignEnd="@+id/profileButton"
|
||||
android:layout_width="@dimen/path_status_view_size"
|
||||
android:layout_height="@dimen/path_status_view_size"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sessionHeaderImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="64dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/very_large_font_size" />
|
||||
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" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/pathStatusViewContainer"
|
||||
android:id="@+id/searchViewContainer"
|
||||
android:layout_width="@dimen/small_profile_picture_size"
|
||||
android:layout_height="@dimen/small_profile_picture_size"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true">
|
||||
|
||||
<org.thoughtcrime.securesms.home.PathStatusView
|
||||
android:layout_width="@dimen/path_status_view_size"
|
||||
android:layout_height="@dimen/path_status_view_size"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="8dp" />
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:src="@drawable/ic_baseline_search_24"
|
||||
app:tint="@color/text" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:visibility="gone"
|
||||
android:id="@+id/search_toolbar"
|
||||
android:layout_marginHorizontal="@dimen/medium_spacing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize">
|
||||
<org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout
|
||||
android:layout_centerVertical="true"
|
||||
android:id="@+id/globalSearchInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?android:dividerHorizontal"
|
||||
android:elevation="1dp" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/seedReminderStub"
|
||||
android:layout="@layout/seed_reminder_stub"
|
||||
<org.thoughtcrime.securesms.onboarding.SeedReminderView
|
||||
android:id="@+id/seedReminderView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
@ -100,6 +121,16 @@
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/home_activity_gradient" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:visibility="gone"
|
||||
android:scrollbars="vertical"
|
||||
android:id="@+id/globalSearchRecycler"
|
||||
android:layout_width="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/view_global_search_result"
|
||||
tools:itemCount="6"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/emptyStateContainer"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -111,6 +111,39 @@
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:background="?android:dividerHorizontal" />
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:id="@+id/pathButton"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
app:justifyContent="center"
|
||||
app:alignItems="center"
|
||||
android:layout_height="@dimen/setting_button_height"
|
||||
android:background="@drawable/setting_button_background">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:layout_gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:text="@string/activity_path_title" />
|
||||
<FrameLayout
|
||||
android:id="@+id/pathContainer"
|
||||
android:layout_width="@dimen/small_profile_picture_size"
|
||||
android:layout_height="@dimen/small_profile_picture_size">
|
||||
<org.thoughtcrime.securesms.home.PathStatusView
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="@dimen/path_status_view_size"
|
||||
android:layout_height="@dimen/path_status_view_size"/>
|
||||
</FrameLayout>
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?android:dividerHorizontal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/privacyButton"
|
||||
android:layout_width="match_parent"
|
||||
@ -275,9 +308,9 @@
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:animateLayoutChanges="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/loader"
|
||||
@ -290,8 +323,8 @@
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginTop="8dp"
|
||||
app:SpinKit_Color="@android:color/white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sms_failed_indicator"
|
||||
@ -8,7 +9,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_error"
|
||||
android:visibility="gone"
|
||||
android:tint="@color/core_red"
|
||||
app:tint="@color/core_red"
|
||||
tools:visibility="visible"
|
||||
android:contentDescription="@string/conversation_item_sent__send_failed_indicator_description" />
|
||||
|
||||
@ -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"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -30,7 +30,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"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pending_indicator"
|
||||
@ -38,7 +39,7 @@
|
||||
android:paddingStart="2dp"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/conversation_item_sent__message_read"
|
||||
android:tint="@color/core_blue"
|
||||
app:tint="@color/core_blue"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
</merge>
|
@ -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"/>
|
||||
|
||||
</FrameLayout>
|
@ -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"
|
||||
|
@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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"
|
||||
android:tint="#99FFFFFF"
|
||||
android:visibility="gone"/>
|
||||
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" />
|
||||
|
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:layout_marginBottom="2dp">
|
||||
|
||||
@ -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"/>
|
||||
|
||||
<TextView
|
||||
|
@ -37,7 +37,7 @@
|
||||
android:layout_height="18dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="2dp"
|
||||
android:tint="@color/core_blue"
|
||||
app:tint="@color/core_blue"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/triangle_right" />
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@ -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"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@ -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"
|
||||
|
@ -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"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
@ -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"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/quote_missing_text"
|
||||
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.onboarding.SeedReminderView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/seedReminderView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
@ -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" />
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:tint="@color/core_grey_60"
|
||||
app:tint="@color/core_grey_60"
|
||||
app:srcCompat="@drawable/ic_arrow_down_circle_filled" />
|
||||
|
||||
<TextView
|
||||
|
@ -77,10 +77,10 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:text="8"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:text="8" />
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
16
app/src/main/res/layout/view_global_search_header.xml
Normal file
16
app/src/main/res/layout/view_global_search_header.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorPrimary"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<TextView
|
||||
tools:text="@string/global_search_messages"
|
||||
android:textSize="18sp"
|
||||
android:fontFamily="@font/roboto_medium"
|
||||
android:paddingVertical="@dimen/medium_spacing"
|
||||
android:paddingHorizontal="@dimen/very_large_spacing"
|
||||
android:id="@+id/search_header"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"/>
|
||||
</LinearLayout>
|
58
app/src/main/res/layout/view_global_search_input.xml
Normal file
58
app/src/main/res/layout/view_global_search_input.xml
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:animateLayoutChanges="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:id="@+id/search_input_parent"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:background="@drawable/search_background"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/search_cancel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<ImageView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@drawable/ic_baseline_search_24"
|
||||
android:scaleType="centerInside"
|
||||
app:tint="?searchIconColor"
|
||||
android:contentDescription="@string/SearchToolbar_search" />
|
||||
<EditText
|
||||
android:hint="@string/global_search_messages"
|
||||
android:imeOptions="actionSearch"
|
||||
android:id="@+id/search_input"
|
||||
android:paddingHorizontal="@dimen/small_spacing"
|
||||
android:textSize="16sp"
|
||||
android:background="@null"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:inputType="text"
|
||||
android:layout_height="wrap_content"/>
|
||||
<ImageView
|
||||
android:id="@+id/search_clear"
|
||||
android:src="@drawable/ic_close_white_18dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
app:tint="?searchIconColor" />
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:layout_marginStart="@dimen/small_spacing"
|
||||
android:layout_centerVertical="true"
|
||||
android:text="@string/cancel"
|
||||
app:layout_constraintStart_toEndOf="@+id/search_input_parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:id="@+id/search_cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
111
app/src/main/res/layout/view_global_search_result.xml
Normal file
111
app/src/main/res/layout/view_global_search_result.xml
Normal file
@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="@color/cell_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<FrameLayout
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/search_result_text_parent"
|
||||
android:layout_marginVertical="@dimen/small_spacing"
|
||||
android:layout_marginStart="@dimen/large_spacing"
|
||||
android:layout_marginEnd="@dimen/large_spacing"
|
||||
android:id="@+id/search_result_profile_picture_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||
android:visibility="gone"
|
||||
android:id="@+id/search_result_profile_picture"
|
||||
android:layout_width="@dimen/medium_profile_picture_size"
|
||||
android:layout_height="@dimen/medium_profile_picture_size"
|
||||
/>
|
||||
<ImageView
|
||||
android:background="@drawable/circle_tintable"
|
||||
android:backgroundTint="?colorAccent"
|
||||
android:src="@drawable/ic_outline_bookmark_border_24"
|
||||
android:padding="4dp"
|
||||
android:id="@+id/search_result_saved_messages"
|
||||
android:layout_width="@dimen/medium_profile_picture_size"
|
||||
android:layout_height="@dimen/medium_profile_picture_size"
|
||||
app:tint="@color/white" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/search_result_text_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/large_spacing"
|
||||
app:layout_constraintStart_toEndOf="@+id/search_result_profile_picture_parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:orientation="vertical">
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:layout_gravity="center"
|
||||
android:textSize="@dimen/text_size"
|
||||
tools:text="@tools:sample/full_names"
|
||||
android:id="@+id/search_result_title"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
/>
|
||||
<RelativeLayout
|
||||
android:id="@+id/unreadCountIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:maxWidth="40dp"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/rounded_rectangle"
|
||||
android:backgroundTint="@color/conversation_unread_count_indicator_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unreadCountTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
tools:text="8"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
android:textColor="@color/text"
|
||||
android:alpha="0.4"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="@dimen/small_spacing"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:id="@+id/search_result_timestamp"
|
||||
tools:text="@tools:sample/date/hhmmss"
|
||||
android:textAlignment="viewEnd"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
tools:text="@tools:sample/full_names"
|
||||
android:textSize="@dimen/text_size"
|
||||
android:id="@+id/search_result_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
16
app/src/main/res/menu/menu_home.xml
Normal file
16
app/src/main/res/menu/menu_home.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/search_action"
|
||||
android:icon="@drawable/ic_baseline_search_24"
|
||||
app:showAsAction="ifRoom"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
android:title="@string/SearchToolbar_search"/>
|
||||
|
||||
<item android:id="@+id/path_action"
|
||||
android:icon="@drawable/accent_dot"
|
||||
android:title="@string/activity_path_title"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
|
||||
</menu>
|
@ -238,7 +238,7 @@ on viallinen!</string>
|
||||
<string name="ThreadRecord_disappearing_messages_disabled">Katoavat viestit poistettu käytöstä</string>
|
||||
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Katoavien viestien ajaksi asetettu %s</string>
|
||||
<string name="ThreadRecord_s_took_a_screenshot">%s otti kuvakaappauksen.</string>
|
||||
<string name="ThreadRecord_media_saved_by_s">Media tallennettu toimesta.</string>
|
||||
<string name="ThreadRecord_media_saved_by_s">Media tallennettu toimesta %s.</string>
|
||||
<string name="ThreadRecord_safety_number_changed">Turvanumero vaihtunut</string>
|
||||
<string name="ThreadRecord_your_safety_number_with_s_has_changed">Sinun ja yhteystiedon %s turvanumero on vaihtunut.</string>
|
||||
<string name="ThreadRecord_you_marked_verified">Merkitsit varmennetuksi.</string>
|
||||
|
@ -238,7 +238,7 @@ on viallinen!</string>
|
||||
<string name="ThreadRecord_disappearing_messages_disabled">Katoavat viestit poistettu käytöstä</string>
|
||||
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Katoavien viestien ajaksi asetettu %s</string>
|
||||
<string name="ThreadRecord_s_took_a_screenshot">%s otti kuvakaappauksen.</string>
|
||||
<string name="ThreadRecord_media_saved_by_s">Media tallennettu toimesta.</string>
|
||||
<string name="ThreadRecord_media_saved_by_s">Media tallennettu toimesta %s.</string>
|
||||
<string name="ThreadRecord_safety_number_changed">Turvanumero vaihtunut</string>
|
||||
<string name="ThreadRecord_your_safety_number_with_s_has_changed">Sinun ja yhteystiedon %s turvanumero on vaihtunut.</string>
|
||||
<string name="ThreadRecord_you_marked_verified">Merkitsit varmennetuksi.</string>
|
||||
|
@ -729,5 +729,4 @@
|
||||
<string name="deleted_message">ये संदेश मिटा दिया है</string>
|
||||
<string name="delete_message_for_me">स्वयं के लिये मिटाये</string>
|
||||
<string name="delete_message_for_everyone">सभी के लिए संदेश मिटायें</string>
|
||||
<string name="delete_message_for_me_and_recipient">स्वयमेव और प्राप्तिकर्ता के लिये मिटाये</string>
|
||||
</resources>
|
||||
|
@ -5,6 +5,10 @@
|
||||
<item name="android:navigationBarColor">?android:navigationBarColor</item>
|
||||
<item name="android:textColorHint">@color/gray50</item>
|
||||
|
||||
<item name="searchBackgroundColor">#F2F2F2</item>
|
||||
<item name="searchIconColor">#413F40</item>
|
||||
<item name="searchHighlightTint">#767676</item>
|
||||
|
||||
<item name="home_gradient_start">#00FFFFFF</item>
|
||||
<item name="home_gradient_end">#FFFFFFFF</item>
|
||||
|
||||
|
@ -237,7 +237,6 @@
|
||||
<string name="ThreadRecord_s_is_on_signal">%s është në Session! </string>
|
||||
<string name="ThreadRecord_disappearing_messages_disabled">Zhdukja e mesazheve është e çaktivizuar</string>
|
||||
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Koha për zhdukje mesazhesh është vënë %s</string>
|
||||
<string name="ThreadRecord_s_took_a_screenshot">Bëri nje screenshot</string>
|
||||
<string name="ThreadRecord_media_saved_by_s">Media u kursye me %s</string>
|
||||
<string name="ThreadRecord_safety_number_changed">Numri i sigurisë ndryshoi</string>
|
||||
<string name="ThreadRecord_your_safety_number_with_s_has_changed">Numri juaj i sigurisë me %s është ndryshuar.</string>
|
||||
|
@ -8,7 +8,7 @@
|
||||
<color name="profile_picture_background">#353535</color>
|
||||
<color name="cell_background">#1B1B1B</color>
|
||||
<color name="cell_selected">#0C0C0C</color>
|
||||
<color name="action_bar_background">#171717</color>
|
||||
<color name="action_bar_background">#161616</color>
|
||||
<color name="separator">#36383C</color>
|
||||
<color name="unimportant_button_background">#323232</color>
|
||||
<color name="dialog_background">#101011</color>
|
||||
|
@ -905,5 +905,7 @@
|
||||
<string name="conversation_pin">Pin</string>
|
||||
<string name="conversation_unpin">Unpin</string>
|
||||
<string name="mark_all_as_read">Mark all as read</string>
|
||||
<string name="global_search_contacts_groups">Contacts and Groups</string>
|
||||
<string name="global_search_messages">Messages</string>
|
||||
|
||||
</resources>
|
||||
|
@ -5,6 +5,9 @@
|
||||
<style name="Base.Theme.Session" parent="@style/Theme.AppCompat.DayNight.DarkActionBar">
|
||||
<item name="colorPrimary">@color/action_bar_background</item>
|
||||
<item name="colorPrimaryDark">@color/action_bar_background</item>
|
||||
<item name="searchBackgroundColor">#1B1B1B</item>
|
||||
<item name="searchIconColor">#E5E5E8</item>
|
||||
<item name="searchHighlightTint">@color/accent</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="colorControlNormal">?android:textColorPrimary</item>
|
||||
<item name="colorControlActivated">?colorAccent</item>
|
||||
|
@ -2,4 +2,7 @@
|
||||
<resources>
|
||||
<bool name="enable_alarm_manager">true</bool>
|
||||
<bool name="enable_job_service">false</bool>
|
||||
<attr name="searchBackgroundColor" format="color"/>
|
||||
<attr name="searchIconColor" format="color"/>
|
||||
<attr name="searchHighlightTint" format="color"/>
|
||||
</resources>
|
||||
|
@ -1,78 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.powermock.api.mockito.PowerMockito;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyBoolean;
|
||||
import static org.mockito.Matchers.anyFloat;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.anyLong;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({ Log.class, Handler.class, Looper.class, TextUtils.class, PreferenceManager.class })
|
||||
public abstract class BaseUnitTest {
|
||||
|
||||
protected Context context = mock(Context.class);
|
||||
protected SharedPreferences sharedPreferences = mock(SharedPreferences.class);
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mockStatic(Looper.class);
|
||||
mockStatic(Log.class);
|
||||
mockStatic(Handler.class);
|
||||
mockStatic(TextUtils.class);
|
||||
mockStatic(PreferenceManager.class);
|
||||
|
||||
when(PreferenceManager.getDefaultSharedPreferences(any(Context.class))).thenReturn(sharedPreferences);
|
||||
when(Looper.getMainLooper()).thenReturn(null);
|
||||
PowerMockito.whenNew(Handler.class).withAnyArguments().thenReturn(null);
|
||||
|
||||
Answer<?> logAnswer = new Answer<Void>() {
|
||||
@Override public Void answer(InvocationOnMock invocation) throws Throwable {
|
||||
final String tag = (String)invocation.getArguments()[0];
|
||||
final String msg = (String)invocation.getArguments()[1];
|
||||
System.out.println(invocation.getMethod().getName().toUpperCase() + "/[" + tag + "] " + msg);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
PowerMockito.doAnswer(logAnswer).when(Log.class, "d", anyString(), anyString());
|
||||
PowerMockito.doAnswer(logAnswer).when(Log.class, "i", anyString(), anyString());
|
||||
PowerMockito.doAnswer(logAnswer).when(Log.class, "w", anyString(), anyString());
|
||||
PowerMockito.doAnswer(logAnswer).when(Log.class, "e", anyString(), anyString());
|
||||
PowerMockito.doAnswer(logAnswer).when(Log.class, "wtf", anyString(), anyString());
|
||||
|
||||
PowerMockito.doAnswer(new Answer<Boolean>() {
|
||||
@Override
|
||||
public Boolean answer(InvocationOnMock invocation) throws Throwable {
|
||||
final String s = (String)invocation.getArguments()[0];
|
||||
return s == null || s.length() == 0;
|
||||
}
|
||||
}).when(TextUtils.class, "isEmpty", anyString());
|
||||
|
||||
when(sharedPreferences.getString(anyString(), anyString())).thenReturn("");
|
||||
when(sharedPreferences.getLong(anyString(), anyLong())).thenReturn(0L);
|
||||
when(sharedPreferences.getInt(anyString(), anyInt())).thenReturn(0);
|
||||
when(sharedPreferences.getBoolean(anyString(), anyBoolean())).thenReturn(false);
|
||||
when(sharedPreferences.getFloat(anyString(), anyFloat())).thenReturn(0f);
|
||||
when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPreferences);
|
||||
when(context.getPackageName()).thenReturn("org.thoughtcrime.securesms");
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -8,6 +8,8 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@ -84,13 +86,14 @@ public class FastJobStorageTest {
|
||||
|
||||
@Test
|
||||
public void updateAllJobsToBePending_allArePending() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, true),
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, true),
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", AvatarDownloadJob.KEY, null, 1, 1, 1, 1, 1, 1, 1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
|
||||
subject.init();
|
||||
@ -134,10 +137,11 @@ public class FastJobStorageTest {
|
||||
|
||||
@Test
|
||||
public void updateJobAfterRetry_stateUpdated() {
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", "f1", null, 0, 0, 0, 3, 30000, -1, -1, EMPTY_DATA, true),
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, null, 0, 0, 0, 3, 30000, -1, -1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec)));
|
||||
|
||||
subject.init();
|
||||
@ -153,13 +157,14 @@ public class FastJobStorageTest {
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenEarlierItemInQueueInRunning() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", AvatarDownloadJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
subject.init();
|
||||
|
||||
@ -168,10 +173,11 @@ public class FastJobStorageTest {
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenAllJobsAreRunning() {
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", "f1", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec)));
|
||||
subject.init();
|
||||
|
||||
@ -180,10 +186,11 @@ public class FastJobStorageTest {
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenNextRunTimeIsAfterCurrentTime() {
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", "f1", "q", 0, 10, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, "q", 0, 10, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec)));
|
||||
subject.init();
|
||||
|
||||
@ -192,14 +199,14 @@ public class FastJobStorageTest {
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_noneWhenDependentOnAnotherJob() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.singletonList(new DependencySpec("2", "1")));
|
||||
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
subject.init();
|
||||
|
||||
@ -208,10 +215,11 @@ public class FastJobStorageTest {
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_singleEligibleJob() {
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", "f1", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
FullSpec fullSpec = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Collections.singletonList(fullSpec)));
|
||||
subject.init();
|
||||
|
||||
@ -220,14 +228,14 @@ public class FastJobStorageTest {
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_multipleEligibleJobs() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
subject.init();
|
||||
|
||||
@ -236,14 +244,14 @@ public class FastJobStorageTest {
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_singleEligibleJobInMixedList() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, true),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", AvatarDownloadJob.KEY, null, 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
subject.init();
|
||||
|
||||
@ -255,14 +263,14 @@ public class FastJobStorageTest {
|
||||
|
||||
@Test
|
||||
public void getPendingJobsWithNoDependenciesInCreatedOrder_firstItemInQueue() {
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", "f1", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
FullSpec fullSpec1 = new FullSpec(new JobSpec("1", RetrieveProfileAvatarJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", "f2", "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
FullSpec fullSpec2 = new FullSpec(new JobSpec("2", RetrieveProfileAvatarJob.KEY, "q", 0, 0, 0, 0, 0, -1, -1, EMPTY_DATA, false),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
|
||||
JobManagerFactories.getJobFactories(mock(Application.class));
|
||||
FastJobStorage subject = new FastJobStorage(fixedDataDatabase(Arrays.asList(fullSpec1, fullSpec2)));
|
||||
subject.init();
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.BaseUnitTest;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.util.LinkedList;
|
||||
@ -9,7 +8,7 @@ import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class ListPartitionTest extends BaseUnitTest {
|
||||
public class ListPartitionTest {
|
||||
|
||||
@Test public void testPartitionEven() {
|
||||
List<Integer> list = new LinkedList<>();
|
||||
|
@ -20,13 +20,12 @@ package org.thoughtcrime.securesms.util;
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.BaseUnitTest;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class Rfc5724UriTest extends BaseUnitTest {
|
||||
public class Rfc5724UriTest {
|
||||
|
||||
@Test public void testInvalidPath() throws Exception {
|
||||
final String[] invalidSchemaUris = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.jvmargs=-Xmx4g
|
||||
|
||||
kotlinVersion=1.6.0
|
||||
coroutinesVersion=1.6.0
|
||||
|
@ -42,8 +42,17 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||
testImplementation "junit:junit:3.8.2"
|
||||
testImplementation "org.assertj:assertj-core:1.7.1"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation "org.mockito:mockito-inline:4.0.0"
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||
testImplementation 'androidx.test:core:1.3.0'
|
||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||
testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0"
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
}
|
@ -45,7 +45,7 @@ class BatchMessageReceiveJob(
|
||||
|
||||
fun executeAsync(): Promise<Unit, Exception> {
|
||||
return task {
|
||||
messages.forEach { messageParameters ->
|
||||
messages.iterator().forEach { messageParameters ->
|
||||
val (data, serverHash, openGroupMessageServerID) = messageParameters
|
||||
try {
|
||||
val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID)
|
||||
|
@ -155,7 +155,7 @@ object MessageSender {
|
||||
var isSuccess = false
|
||||
val promiseCount = promises.size
|
||||
var errorCount = 0
|
||||
promises.forEach { promise: RawResponsePromise ->
|
||||
promises.iterator().forEach { promise: RawResponsePromise ->
|
||||
promise.success {
|
||||
if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds
|
||||
isSuccess = true
|
||||
|
@ -236,7 +236,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.DuplicateMessage
|
||||
// Parse & persist attachments
|
||||
// Start attachment downloads if needed
|
||||
storage.getAttachmentsForMessage(messageID).forEach { attachment ->
|
||||
storage.getAttachmentsForMessage(messageID).iterator().forEach { attachment ->
|
||||
attachment.attachmentId?.let { id ->
|
||||
val downloadJob = AttachmentDownloadJob(id.rowId, messageID)
|
||||
JobQueue.shared.add(downloadJob)
|
||||
|
@ -52,7 +52,7 @@ object PushNotificationAPI {
|
||||
// Unsubscribe from all closed groups
|
||||
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
allClosedGroupPublicKeys.forEach { closedGroup ->
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||
}
|
||||
}
|
||||
@ -81,7 +81,7 @@ object PushNotificationAPI {
|
||||
}
|
||||
// Subscribe to all closed groups
|
||||
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
allClosedGroupPublicKeys.forEach { closedGroup ->
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class ClosedGroupPollerV2 {
|
||||
fun start() {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
allGroupPublicKeys.forEach { startPolling(it) }
|
||||
allGroupPublicKeys.iterator().forEach { startPolling(it) }
|
||||
}
|
||||
|
||||
fun startPolling(groupPublicKey: String) {
|
||||
@ -51,7 +51,7 @@ class ClosedGroupPollerV2 {
|
||||
fun stop() {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||
allGroupPublicKeys.forEach { stopPolling(it) }
|
||||
allGroupPublicKeys.iterator().forEach { stopPolling(it) }
|
||||
}
|
||||
|
||||
fun stopPolling(groupPublicKey: String) {
|
||||
@ -100,7 +100,7 @@ class ClosedGroupPollerV2 {
|
||||
}
|
||||
promise.success { envelopes ->
|
||||
if (!isPolling(groupPublicKey)) { return@success }
|
||||
envelopes.forEach { (envelope, serverHash) ->
|
||||
envelopes.iterator().forEach { (envelope, serverHash) ->
|
||||
val job = MessageReceiveJob(envelope.toByteArray(), serverHash)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.min
|
||||
|
||||
object Util {
|
||||
@Volatile
|
||||
@ -216,7 +217,7 @@ object Util {
|
||||
val results: MutableList<List<T>> = LinkedList()
|
||||
var index = 0
|
||||
while (index < list.size) {
|
||||
val subListSize = Math.min(partitionSize, list.size - index)
|
||||
val subListSize = min(partitionSize, list.size - index)
|
||||
results.add(list.subList(index, index + subListSize))
|
||||
index += partitionSize
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
<resources>
|
||||
|
||||
<string-array name="language_entries">
|
||||
<item>@string/preferences__default</item>
|
||||
<item>English</item>
|
||||
<item>Arabic العربية</item>
|
||||
<item>Azərbaycan</item>
|
||||
@ -119,18 +118,6 @@
|
||||
<item>vi</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_led_color_entries">
|
||||
<item>@string/preferences__green</item>
|
||||
<item>@string/preferences__red</item>
|
||||
<item>@string/preferences__blue</item>
|
||||
<item>@string/preferences__orange</item>
|
||||
<item>@string/preferences__cyan</item>
|
||||
<item>@string/preferences__magenta</item>
|
||||
<item>@string/preferences__white</item>
|
||||
<item>@string/preferences__none</item>
|
||||
</string-array>
|
||||
|
||||
|
||||
<string-array name="pref_led_color_values" translatable="false">
|
||||
<item>green</item>
|
||||
<item>red</item>
|
||||
@ -142,27 +129,12 @@
|
||||
<item>none</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_led_blink_pattern_entries">
|
||||
<item>@string/preferences__fast</item>
|
||||
<item>@string/preferences__normal</item>
|
||||
<item>@string/preferences__slow</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_led_blink_pattern_values" translatable="false">
|
||||
<item>300,300</item>
|
||||
<item>500,2000</item>
|
||||
<item>3000,3000</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_repeat_alerts_entries">
|
||||
<item>@string/preferences__never</item>
|
||||
<item>@string/preferences__one_time</item>
|
||||
<item>@string/preferences__two_times</item>
|
||||
<item>@string/preferences__three_times</item>
|
||||
<item>@string/preferences__five_times</item>
|
||||
<item>@string/preferences__ten_times</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_repeat_alerts_values" translatable="false">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
@ -177,25 +149,6 @@
|
||||
<item>custom</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="default_or_custom_entries">
|
||||
<item>@string/arrays__use_default</item>
|
||||
<item>@string/arrays__use_custom</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mute_durations">
|
||||
<item>@string/arrays__mute_for_one_hour</item>
|
||||
<item>@string/arrays__mute_for_two_hours</item>
|
||||
<item>@string/arrays__mute_for_one_day</item>
|
||||
<item>@string/arrays__mute_for_seven_days</item>
|
||||
<item>@string/arrays__mute_for_one_year</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_notification_privacy_entries">
|
||||
<item>@string/arrays__name_and_message</item>
|
||||
<item>@string/arrays__name_only</item>
|
||||
<item>@string/arrays__no_name_or_message</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_notification_privacy_values">
|
||||
<item>all</item>
|
||||
<item>contact</item>
|
||||
@ -247,12 +200,6 @@
|
||||
<item>#000000</item>
|
||||
</array>
|
||||
|
||||
<string-array name="pref_notification_priority_entries">
|
||||
<item>@string/arrays__default</item>
|
||||
<item>@string/arrays__high</item>
|
||||
<item>@string/arrays__max</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_notification_priority_values">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
|
@ -1,17 +0,0 @@
|
||||
package org.session.libsignal
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user