Merge pull request #2130 from vector-im/feature/search_in_room
Search messages in a room
This commit is contained in:
commit
c996bcb23c
@ -2,6 +2,7 @@ Changes in Element 1.0.9 (2020-XX-XX)
|
|||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
|
- Search messages in a room - phase 1 (#2110)
|
||||||
- Hide encrypted history (before user is invited). Can be shown if wanted in developer settings
|
- Hide encrypted history (before user is invited). Can be shown if wanted in developer settings
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
@ -19,7 +20,7 @@ Translations 🗣:
|
|||||||
-
|
-
|
||||||
|
|
||||||
SDK API changes ⚠️:
|
SDK API changes ⚠️:
|
||||||
-
|
- Search messages in a room by using Session.searchService() or Room.search()
|
||||||
|
|
||||||
Build 🧱:
|
Build 🧱:
|
||||||
- Use Update Gradle Wrapper Action
|
- Use Update Gradle Wrapper Action
|
||||||
|
@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.session.search
|
||||||
|
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Assert.fail
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.JUnit4
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||||
|
import org.matrix.android.sdk.common.CommonTestHelper
|
||||||
|
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
|
class SearchMessagesTest : InstrumentedTest {
|
||||||
|
|
||||||
|
private val MESSAGE = "Lorem ipsum dolor sit amet"
|
||||||
|
|
||||||
|
private val commonTestHelper = CommonTestHelper(context())
|
||||||
|
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sendTextMessageAndSearchPartOfItUsingSession() {
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||||
|
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(10))
|
||||||
|
aliceTimeline.start()
|
||||||
|
|
||||||
|
commonTestHelper.sendTextMessage(
|
||||||
|
roomFromAlicePOV,
|
||||||
|
MESSAGE,
|
||||||
|
2)
|
||||||
|
|
||||||
|
run {
|
||||||
|
var lock = CountDownLatch(1)
|
||||||
|
|
||||||
|
val eventListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
snapshot.count { it.root.content.toModel<MessageContent>()?.body?.startsWith(MESSAGE).orFalse() } == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceTimeline.addListener(eventListener)
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
|
||||||
|
lock = CountDownLatch(1)
|
||||||
|
aliceSession
|
||||||
|
.searchService()
|
||||||
|
.search(
|
||||||
|
searchTerm = "lore",
|
||||||
|
limit = 10,
|
||||||
|
includeProfile = true,
|
||||||
|
afterLimit = 0,
|
||||||
|
beforeLimit = 10,
|
||||||
|
orderByRecent = true,
|
||||||
|
nextBatch = null,
|
||||||
|
roomId = aliceRoomId,
|
||||||
|
callback = object : MatrixCallback<SearchResult> {
|
||||||
|
override fun onSuccess(data: SearchResult) {
|
||||||
|
super.onSuccess(data)
|
||||||
|
assertTrue(data.results?.size == 2)
|
||||||
|
assertTrue(
|
||||||
|
data.results
|
||||||
|
?.all {
|
||||||
|
(it.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse()
|
||||||
|
}.orFalse()
|
||||||
|
)
|
||||||
|
lock.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
super.onFailure(failure)
|
||||||
|
fail(failure.localizedMessage)
|
||||||
|
lock.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lock.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
|
||||||
|
|
||||||
|
aliceTimeline.removeAllListeners()
|
||||||
|
cryptoTestData.cleanUp(commonTestHelper)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceSession.startSync(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sendTextMessageAndSearchPartOfItUsingRoom() {
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||||
|
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(10))
|
||||||
|
aliceTimeline.start()
|
||||||
|
|
||||||
|
commonTestHelper.sendTextMessage(
|
||||||
|
roomFromAlicePOV,
|
||||||
|
MESSAGE,
|
||||||
|
2)
|
||||||
|
|
||||||
|
run {
|
||||||
|
var lock = CountDownLatch(1)
|
||||||
|
|
||||||
|
val eventListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
snapshot.count { it.root.content.toModel<MessageContent>()?.body?.startsWith(MESSAGE).orFalse() } == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceTimeline.addListener(eventListener)
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
|
||||||
|
lock = CountDownLatch(1)
|
||||||
|
roomFromAlicePOV
|
||||||
|
.search(
|
||||||
|
searchTerm = "lore",
|
||||||
|
limit = 10,
|
||||||
|
includeProfile = true,
|
||||||
|
afterLimit = 0,
|
||||||
|
beforeLimit = 10,
|
||||||
|
orderByRecent = true,
|
||||||
|
nextBatch = null,
|
||||||
|
callback = object : MatrixCallback<SearchResult> {
|
||||||
|
override fun onSuccess(data: SearchResult) {
|
||||||
|
super.onSuccess(data)
|
||||||
|
assertTrue(data.results?.size == 2)
|
||||||
|
assertTrue(
|
||||||
|
data.results
|
||||||
|
?.all {
|
||||||
|
(it.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse()
|
||||||
|
}.orFalse()
|
||||||
|
)
|
||||||
|
lock.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
super.onFailure(failure)
|
||||||
|
fail(failure.localizedMessage)
|
||||||
|
lock.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lock.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
|
||||||
|
|
||||||
|
aliceTimeline.removeAllListeners()
|
||||||
|
cryptoTestData.cleanUp(commonTestHelper)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceSession.startSync(true)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="org.matrix.android.sdk">
|
package="org.matrix.android.sdk">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@ -7,6 +8,15 @@
|
|||||||
|
|
||||||
<application android:networkSecurityConfig="@xml/network_security_config">
|
<application android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This is mandatory to run integration tests
|
||||||
|
-->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||||
|
android:authorities="${applicationId}.workmanager-init"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The SDK offers a secured File provider to access downloaded files.
|
The SDK offers a secured File provider to access downloaded files.
|
||||||
Access to these file will be given via the FileService, with a temporary
|
Access to these file will be given via the FileService, with a temporary
|
||||||
|
@ -41,6 +41,7 @@ import org.matrix.android.sdk.api.session.profile.ProfileService
|
|||||||
import org.matrix.android.sdk.api.session.pushers.PushersService
|
import org.matrix.android.sdk.api.session.pushers.PushersService
|
||||||
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
||||||
import org.matrix.android.sdk.api.session.room.RoomService
|
import org.matrix.android.sdk.api.session.room.RoomService
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchService
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||||
import org.matrix.android.sdk.api.session.signout.SignOutService
|
import org.matrix.android.sdk.api.session.signout.SignOutService
|
||||||
@ -201,6 +202,11 @@ interface Session :
|
|||||||
*/
|
*/
|
||||||
fun permalinkService(): PermalinkService
|
fun permalinkService(): PermalinkService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the search service associated with the session
|
||||||
|
*/
|
||||||
|
fun searchService(): SearchService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a listener to the session.
|
* Add a listener to the session.
|
||||||
* @param listener the listener to add.
|
* @param listener the listener to add.
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
package org.matrix.android.sdk.api.session.room
|
package org.matrix.android.sdk.api.session.room
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.room.call.RoomCallService
|
import org.matrix.android.sdk.api.session.room.call.RoomCallService
|
||||||
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
|
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
|
||||||
import org.matrix.android.sdk.api.session.room.members.MembershipService
|
import org.matrix.android.sdk.api.session.room.members.MembershipService
|
||||||
@ -33,6 +34,8 @@ import org.matrix.android.sdk.api.session.room.tags.TagsService
|
|||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||||
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
||||||
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||||
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,4 +72,25 @@ interface Room :
|
|||||||
* A current snapshot of [RoomSummary] associated with the room
|
* A current snapshot of [RoomSummary] associated with the room
|
||||||
*/
|
*/
|
||||||
fun roomSummary(): RoomSummary?
|
fun roomSummary(): RoomSummary?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic function to search a term in a room.
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/latest#module-search
|
||||||
|
* @param searchTerm the term to search
|
||||||
|
* @param nextBatch the token that retrieved from the previous response. Should be provided to get the next batch of results
|
||||||
|
* @param orderByRecent if true, the most recent message events will return in the first places of the list
|
||||||
|
* @param limit the maximum number of events to return.
|
||||||
|
* @param beforeLimit how many events before the result are returned.
|
||||||
|
* @param afterLimit how many events after the result are returned.
|
||||||
|
* @param includeProfile requests that the server returns the historic profile information for the users that sent the events that were returned.
|
||||||
|
* @param callback Callback to get the search result
|
||||||
|
*/
|
||||||
|
fun search(searchTerm: String,
|
||||||
|
nextBatch: String?,
|
||||||
|
orderByRecent: Boolean,
|
||||||
|
limit: Int,
|
||||||
|
beforeLimit: Int,
|
||||||
|
afterLimit: Int,
|
||||||
|
includeProfile: Boolean,
|
||||||
|
callback: MatrixCallback<SearchResult>): Cancelable
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.session.search
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain class to represent the response of a search request in a room.
|
||||||
|
*/
|
||||||
|
data class SearchResult(
|
||||||
|
/**
|
||||||
|
* Token that can be used to get the next batch of results, by passing as the next_batch parameter to the next call.
|
||||||
|
* If this field is null, there are no more results.
|
||||||
|
*/
|
||||||
|
val nextBatch: String? = null,
|
||||||
|
/**
|
||||||
|
* List of words which should be highlighted, useful for stemming which may change the query terms.
|
||||||
|
*/
|
||||||
|
val highlights: List<String>? = null,
|
||||||
|
/**
|
||||||
|
* List of results in the requested order.
|
||||||
|
*/
|
||||||
|
val results: List<EventAndSender>? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EventAndSender(
|
||||||
|
val event: Event,
|
||||||
|
val sender: MatrixItem.UserItem?
|
||||||
|
)
|
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.session.search
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface defines methods to search messages in rooms.
|
||||||
|
*/
|
||||||
|
interface SearchService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic function to search a term in a room.
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/latest#module-search
|
||||||
|
* @param searchTerm the term to search
|
||||||
|
* @param roomId the roomId to search term inside
|
||||||
|
* @param nextBatch the token that retrieved from the previous response. Should be provided to get the next batch of results
|
||||||
|
* @param orderByRecent if true, the most recent message events will return in the first places of the list
|
||||||
|
* @param limit the maximum number of events to return.
|
||||||
|
* @param beforeLimit how many events before the result are returned.
|
||||||
|
* @param afterLimit how many events after the result are returned.
|
||||||
|
* @param includeProfile requests that the server returns the historic profile information for the users that sent the events that were returned.
|
||||||
|
* @param callback Callback to get the search result
|
||||||
|
*/
|
||||||
|
fun search(searchTerm: String,
|
||||||
|
roomId: String,
|
||||||
|
nextBatch: String?,
|
||||||
|
orderByRecent: Boolean,
|
||||||
|
limit: Int,
|
||||||
|
beforeLimit: Int,
|
||||||
|
afterLimit: Int,
|
||||||
|
includeProfile: Boolean,
|
||||||
|
callback: MatrixCallback<SearchResult>): Cancelable
|
||||||
|
}
|
@ -49,6 +49,7 @@ import org.matrix.android.sdk.api.session.profile.ProfileService
|
|||||||
import org.matrix.android.sdk.api.session.pushers.PushersService
|
import org.matrix.android.sdk.api.session.pushers.PushersService
|
||||||
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
||||||
import org.matrix.android.sdk.api.session.room.RoomService
|
import org.matrix.android.sdk.api.session.room.RoomService
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchService
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||||
import org.matrix.android.sdk.api.session.signout.SignOutService
|
import org.matrix.android.sdk.api.session.signout.SignOutService
|
||||||
@ -95,6 +96,7 @@ internal class DefaultSession @Inject constructor(
|
|||||||
private val pushRuleService: Lazy<PushRuleService>,
|
private val pushRuleService: Lazy<PushRuleService>,
|
||||||
private val pushersService: Lazy<PushersService>,
|
private val pushersService: Lazy<PushersService>,
|
||||||
private val termsService: Lazy<TermsService>,
|
private val termsService: Lazy<TermsService>,
|
||||||
|
private val searchService: Lazy<SearchService>,
|
||||||
private val cryptoService: Lazy<DefaultCryptoService>,
|
private val cryptoService: Lazy<DefaultCryptoService>,
|
||||||
private val defaultFileService: Lazy<FileService>,
|
private val defaultFileService: Lazy<FileService>,
|
||||||
private val permalinkService: Lazy<PermalinkService>,
|
private val permalinkService: Lazy<PermalinkService>,
|
||||||
@ -264,6 +266,8 @@ internal class DefaultSession @Inject constructor(
|
|||||||
|
|
||||||
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
||||||
|
|
||||||
|
override fun searchService(): SearchService = searchService.get()
|
||||||
|
|
||||||
override fun getOkHttpClient(): OkHttpClient {
|
override fun getOkHttpClient(): OkHttpClient {
|
||||||
return unauthenticatedWithCertificateOkHttpClient.get()
|
return unauthenticatedWithCertificateOkHttpClient.get()
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,7 @@ import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker
|
|||||||
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
|
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
|
||||||
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
|
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
|
||||||
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
|
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
|
||||||
|
import org.matrix.android.sdk.internal.session.search.SearchModule
|
||||||
import org.matrix.android.sdk.internal.session.signout.SignOutModule
|
import org.matrix.android.sdk.internal.session.signout.SignOutModule
|
||||||
import org.matrix.android.sdk.internal.session.sync.SyncModule
|
import org.matrix.android.sdk.internal.session.sync.SyncModule
|
||||||
import org.matrix.android.sdk.internal.session.sync.SyncTask
|
import org.matrix.android.sdk.internal.session.sync.SyncTask
|
||||||
@ -86,7 +87,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
|||||||
ProfileModule::class,
|
ProfileModule::class,
|
||||||
SessionAssistedInjectModule::class,
|
SessionAssistedInjectModule::class,
|
||||||
AccountModule::class,
|
AccountModule::class,
|
||||||
CallModule::class
|
CallModule::class,
|
||||||
|
SearchModule::class
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
@ -36,10 +36,13 @@ import org.matrix.android.sdk.api.session.room.tags.TagsService
|
|||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||||
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
||||||
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||||
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
|
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
|
||||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
|
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
|
||||||
|
import org.matrix.android.sdk.internal.session.search.SearchTask
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
import org.matrix.android.sdk.internal.task.configureWith
|
import org.matrix.android.sdk.internal.task.configureWith
|
||||||
import java.security.InvalidParameterException
|
import java.security.InvalidParameterException
|
||||||
@ -62,7 +65,8 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
|||||||
private val roomMembersService: MembershipService,
|
private val roomMembersService: MembershipService,
|
||||||
private val roomPushRuleService: RoomPushRuleService,
|
private val roomPushRuleService: RoomPushRuleService,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val sendStateTask: SendStateTask) :
|
private val sendStateTask: SendStateTask,
|
||||||
|
private val searchTask: SearchTask) :
|
||||||
Room,
|
Room,
|
||||||
TimelineService by timelineService,
|
TimelineService by timelineService,
|
||||||
SendService by sendService,
|
SendService by sendService,
|
||||||
@ -123,4 +127,27 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun search(searchTerm: String,
|
||||||
|
nextBatch: String?,
|
||||||
|
orderByRecent: Boolean,
|
||||||
|
limit: Int,
|
||||||
|
beforeLimit: Int,
|
||||||
|
afterLimit: Int,
|
||||||
|
includeProfile: Boolean,
|
||||||
|
callback: MatrixCallback<SearchResult>): Cancelable {
|
||||||
|
return searchTask
|
||||||
|
.configureWith(SearchTask.Params(
|
||||||
|
searchTerm = searchTerm,
|
||||||
|
roomId = roomId,
|
||||||
|
nextBatch = nextBatch,
|
||||||
|
orderByRecent = orderByRecent,
|
||||||
|
limit = limit,
|
||||||
|
beforeLimit = beforeLimit,
|
||||||
|
afterLimit = afterLimit,
|
||||||
|
includeProfile = includeProfile
|
||||||
|
)) {
|
||||||
|
this.callback = callback
|
||||||
|
}.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService
|
|||||||
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
|
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
|
||||||
import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
|
import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
|
||||||
import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
|
import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
|
||||||
|
import org.matrix.android.sdk.internal.session.search.SearchTask
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -59,7 +60,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
|
|||||||
private val membershipServiceFactory: DefaultMembershipService.Factory,
|
private val membershipServiceFactory: DefaultMembershipService.Factory,
|
||||||
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory,
|
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val sendStateTask: SendStateTask) :
|
private val sendStateTask: SendStateTask,
|
||||||
|
private val searchTask: SearchTask) :
|
||||||
RoomFactory {
|
RoomFactory {
|
||||||
|
|
||||||
override fun create(roomId: String): Room {
|
override fun create(roomId: String): Room {
|
||||||
@ -81,7 +83,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
|
|||||||
roomMembersService = membershipServiceFactory.create(roomId),
|
roomMembersService = membershipServiceFactory.create(roomId),
|
||||||
roomPushRuleService = roomPushRuleServiceFactory.create(roomId),
|
roomPushRuleService = roomPushRuleServiceFactory.create(roomId),
|
||||||
taskExecutor = taskExecutor,
|
taskExecutor = taskExecutor,
|
||||||
sendStateTask = sendStateTask
|
sendStateTask = sendStateTask,
|
||||||
|
searchTask = searchTask
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchService
|
||||||
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
|
import javax.inject.Inject
|
||||||
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
|
import org.matrix.android.sdk.internal.task.configureWith
|
||||||
|
|
||||||
|
internal class DefaultSearchService @Inject constructor(
|
||||||
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val searchTask: SearchTask
|
||||||
|
) : SearchService {
|
||||||
|
|
||||||
|
override fun search(searchTerm: String,
|
||||||
|
roomId: String,
|
||||||
|
nextBatch: String?,
|
||||||
|
orderByRecent: Boolean,
|
||||||
|
limit: Int,
|
||||||
|
beforeLimit: Int,
|
||||||
|
afterLimit: Int,
|
||||||
|
includeProfile: Boolean,
|
||||||
|
callback: MatrixCallback<SearchResult>): Cancelable {
|
||||||
|
return searchTask
|
||||||
|
.configureWith(SearchTask.Params(
|
||||||
|
searchTerm = searchTerm,
|
||||||
|
roomId = roomId,
|
||||||
|
nextBatch = nextBatch,
|
||||||
|
orderByRecent = orderByRecent,
|
||||||
|
limit = limit,
|
||||||
|
beforeLimit = beforeLimit,
|
||||||
|
afterLimit = afterLimit,
|
||||||
|
includeProfile = includeProfile
|
||||||
|
)) {
|
||||||
|
this.callback = callback
|
||||||
|
}.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||||
|
import org.matrix.android.sdk.internal.session.search.request.SearchRequestBody
|
||||||
|
import org.matrix.android.sdk.internal.session.search.response.SearchResponse
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
internal interface SearchAPI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a full text search across different categories.
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-search
|
||||||
|
*/
|
||||||
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "search")
|
||||||
|
fun search(@Query("next_batch") nextBatch: String?,
|
||||||
|
@Body body: SearchRequestBody): Call<SearchResponse>
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchService
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
|
@Module
|
||||||
|
internal abstract class SearchModule {
|
||||||
|
|
||||||
|
@Module
|
||||||
|
companion object {
|
||||||
|
@Provides
|
||||||
|
@JvmStatic
|
||||||
|
@SessionScope
|
||||||
|
fun providesSearchAPI(retrofit: Retrofit): SearchAPI {
|
||||||
|
return retrofit.create(SearchAPI::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindSearchService(service: DefaultSearchService): SearchService
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindSearchTask(task: DefaultSearchTask): SearchTask
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search
|
||||||
|
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.matrix.android.sdk.api.session.search.EventAndSender
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.session.search.request.SearchRequestBody
|
||||||
|
import org.matrix.android.sdk.internal.session.search.request.SearchRequestCategories
|
||||||
|
import org.matrix.android.sdk.internal.session.search.request.SearchRequestEventContext
|
||||||
|
import org.matrix.android.sdk.internal.session.search.request.SearchRequestFilter
|
||||||
|
import org.matrix.android.sdk.internal.session.search.request.SearchRequestOrder
|
||||||
|
import org.matrix.android.sdk.internal.session.search.request.SearchRequestRoomEvents
|
||||||
|
import org.matrix.android.sdk.internal.session.search.response.SearchResponse
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface SearchTask : Task<SearchTask.Params, SearchResult> {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val searchTerm: String,
|
||||||
|
val roomId: String,
|
||||||
|
val nextBatch: String? = null,
|
||||||
|
val orderByRecent: Boolean,
|
||||||
|
val limit: Int,
|
||||||
|
val beforeLimit: Int,
|
||||||
|
val afterLimit: Int,
|
||||||
|
val includeProfile: Boolean
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultSearchTask @Inject constructor(
|
||||||
|
private val searchAPI: SearchAPI,
|
||||||
|
private val eventBus: EventBus
|
||||||
|
) : SearchTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: SearchTask.Params): SearchResult {
|
||||||
|
return executeRequest<SearchResponse>(eventBus) {
|
||||||
|
val searchRequestBody = SearchRequestBody(
|
||||||
|
searchCategories = SearchRequestCategories(
|
||||||
|
roomEvents = SearchRequestRoomEvents(
|
||||||
|
searchTerm = params.searchTerm,
|
||||||
|
orderBy = if (params.orderByRecent) SearchRequestOrder.RECENT else SearchRequestOrder.RANK,
|
||||||
|
filter = SearchRequestFilter(
|
||||||
|
limit = params.limit,
|
||||||
|
rooms = listOf(params.roomId)
|
||||||
|
),
|
||||||
|
eventContext = SearchRequestEventContext(
|
||||||
|
beforeLimit = params.beforeLimit,
|
||||||
|
afterLimit = params.afterLimit,
|
||||||
|
includeProfile = params.includeProfile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
apiCall = searchAPI.search(params.nextBatch, searchRequestBody)
|
||||||
|
}.toDomain()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SearchResponse.toDomain(): SearchResult {
|
||||||
|
return SearchResult(
|
||||||
|
nextBatch = searchCategories.roomEvents?.nextBatch,
|
||||||
|
highlights = searchCategories.roomEvents?.highlights,
|
||||||
|
results = searchCategories.roomEvents?.results?.map { searchResponseItem ->
|
||||||
|
EventAndSender(
|
||||||
|
searchResponseItem.event,
|
||||||
|
searchResponseItem.event.senderId?.let { senderId ->
|
||||||
|
searchResponseItem.context?.profileInfo?.get(senderId)
|
||||||
|
?.let {
|
||||||
|
MatrixItem.UserItem(
|
||||||
|
senderId,
|
||||||
|
it["displayname"] as? String,
|
||||||
|
it["avatar_url"] as? String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}?.reversed()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.request
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SearchRequestBody(
|
||||||
|
/**
|
||||||
|
* Required. Describes which categories to search in and their criteria.
|
||||||
|
*/
|
||||||
|
@Json(name = "search_categories")
|
||||||
|
val searchCategories: SearchRequestCategories
|
||||||
|
)
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.request
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SearchRequestCategories(
|
||||||
|
/**
|
||||||
|
* Mapping of category name to search criteria.
|
||||||
|
*/
|
||||||
|
@Json(name = "room_events")
|
||||||
|
val roomEvents: SearchRequestRoomEvents? = null
|
||||||
|
)
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.request
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SearchRequestEventContext(
|
||||||
|
// How many events before the result are returned.
|
||||||
|
@Json(name = "before_limit")
|
||||||
|
val beforeLimit: Int? = null,
|
||||||
|
// How many events after the result are returned.
|
||||||
|
@Json(name = "after_limit")
|
||||||
|
val afterLimit: Int? = null,
|
||||||
|
// Requests that the server returns the historic profile information
|
||||||
|
@Json(name = "include_profile")
|
||||||
|
val includeProfile: Boolean? = null
|
||||||
|
)
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.request
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SearchRequestFilter(
|
||||||
|
// The maximum number of events to return.
|
||||||
|
@Json(name = "limit")
|
||||||
|
val limit: Int? = null,
|
||||||
|
// A list of room IDs to include. If this list is absent then all rooms are included.
|
||||||
|
@Json(name = "rooms")
|
||||||
|
val rooms: List<String>? = null
|
||||||
|
)
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.request
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the order in which to search for results.
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = false)
|
||||||
|
internal enum class SearchRequestOrder {
|
||||||
|
@Json(name = "rank") RANK,
|
||||||
|
@Json(name = "recent") RECENT
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.request
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SearchRequestRoomEvents(
|
||||||
|
/**
|
||||||
|
* Required. The string to search events for.
|
||||||
|
*/
|
||||||
|
@Json(name = "search_term")
|
||||||
|
val searchTerm: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The keys to search. Defaults to all. One of: ["content.body", "content.name", "content.topic"]
|
||||||
|
*/
|
||||||
|
@Json(name = "keys")
|
||||||
|
val keys: Any? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This takes a filter.
|
||||||
|
*/
|
||||||
|
@Json(name = "filter")
|
||||||
|
val filter: SearchRequestFilter? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The order in which to search for results. By default, this is "rank". One of: ["recent", "rank"]
|
||||||
|
*/
|
||||||
|
@Json(name = "order_by")
|
||||||
|
val orderBy: SearchRequestOrder? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures whether any context for the events returned are included in the response.
|
||||||
|
*/
|
||||||
|
@Json(name = "event_context")
|
||||||
|
val eventContext: SearchRequestEventContext? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the server return the current state for each room returned.
|
||||||
|
*/
|
||||||
|
@Json(name = "include_state")
|
||||||
|
val include_state: Boolean? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests that the server partitions the result set based on the provided list of keys.
|
||||||
|
*/
|
||||||
|
// val groupings: SearchRequestGroupings? = null
|
||||||
|
)
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SearchResponse(
|
||||||
|
/**
|
||||||
|
* Required. Describes which categories to search in and their criteria.
|
||||||
|
*/
|
||||||
|
@Json(name = "search_categories")
|
||||||
|
val searchCategories: SearchResponseCategories
|
||||||
|
)
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SearchResponseCategories(
|
||||||
|
/**
|
||||||
|
* Mapping of category name to search criteria.
|
||||||
|
*/
|
||||||
|
@Json(name = "room_events")
|
||||||
|
val roomEvents: SearchResponseRoomEvents? = null
|
||||||
|
)
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SearchResponseEventContext(
|
||||||
|
// Events just before the result.
|
||||||
|
@Json(name = "events_before")
|
||||||
|
val eventsBefore: List<Event>,
|
||||||
|
// Events just after the result.
|
||||||
|
@Json(name = "events_after")
|
||||||
|
val eventsAfter: List<Event>,
|
||||||
|
// Pagination token for the start of the chunk
|
||||||
|
@Json(name = "start")
|
||||||
|
val start: String? = null,
|
||||||
|
// Pagination token for the end of the chunk
|
||||||
|
@Json(name = "end")
|
||||||
|
val end: String? = null,
|
||||||
|
// The historic profile information of the users that sent the events returned. The string key is the user ID for which the profile belongs to.
|
||||||
|
@Json(name = "profile_info")
|
||||||
|
val profileInfo: Map<String, JsonDict>? = null
|
||||||
|
)
|
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SearchResponseItem(
|
||||||
|
/**
|
||||||
|
* A number that describes how closely this result matches the search. Higher is closer.
|
||||||
|
*/
|
||||||
|
@Json(name = "rank")
|
||||||
|
val rank: Double? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event that matched.
|
||||||
|
*/
|
||||||
|
@Json(name = "result")
|
||||||
|
val event: Event,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context for result, if requested.
|
||||||
|
*/
|
||||||
|
@Json(name = "context")
|
||||||
|
val context: SearchResponseEventContext? = null
|
||||||
|
)
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.search.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class SearchResponseRoomEvents(
|
||||||
|
// List of results in the requested order.
|
||||||
|
@Json(name = "results")
|
||||||
|
val results: List<SearchResponseItem>? = null,
|
||||||
|
@Json(name = "count")
|
||||||
|
val count: Int? = null,
|
||||||
|
// List of words which should be highlighted, useful for stemming which may change the query terms.
|
||||||
|
@Json(name = "highlights")
|
||||||
|
val highlights: List<String>? = null,
|
||||||
|
// Token that can be used to get the next batch of results, by passing as the next_batch parameter to the next call. If this field is absent, there are no more results.
|
||||||
|
@Json(name = "next_batch")
|
||||||
|
val nextBatch: String? = null
|
||||||
|
)
|
@ -219,6 +219,7 @@
|
|||||||
<activity android:name=".features.terms.ReviewTermsActivity" />
|
<activity android:name=".features.terms.ReviewTermsActivity" />
|
||||||
<activity android:name=".features.widgets.WidgetActivity" />
|
<activity android:name=".features.widgets.WidgetActivity" />
|
||||||
<activity android:name=".features.pin.PinActivity" />
|
<activity android:name=".features.pin.PinActivity" />
|
||||||
|
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
||||||
|
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
|
|
||||||
|
@ -52,6 +52,7 @@ import im.vector.app.features.home.HomeDrawerFragment
|
|||||||
import im.vector.app.features.home.LoadingFragment
|
import im.vector.app.features.home.LoadingFragment
|
||||||
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
|
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailFragment
|
import im.vector.app.features.home.room.detail.RoomDetailFragment
|
||||||
|
import im.vector.app.features.home.room.detail.search.SearchFragment
|
||||||
import im.vector.app.features.home.room.list.RoomListFragment
|
import im.vector.app.features.home.room.list.RoomListFragment
|
||||||
import im.vector.app.features.login.LoginCaptchaFragment
|
import im.vector.app.features.login.LoginCaptchaFragment
|
||||||
import im.vector.app.features.login.LoginFragment
|
import im.vector.app.features.login.LoginFragment
|
||||||
@ -570,4 +571,9 @@ interface FragmentModule {
|
|||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(RoomBannedMemberListFragment::class)
|
@FragmentKey(RoomBannedMemberListFragment::class)
|
||||||
fun bindRoomBannedMemberListFragment(fragment: RoomBannedMemberListFragment): Fragment
|
fun bindRoomBannedMemberListFragment(fragment: RoomBannedMemberListFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(SearchFragment::class)
|
||||||
|
fun bindSearchFragment(fragment: SearchFragment): Fragment
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ import im.vector.app.features.home.HomeActivity
|
|||||||
import im.vector.app.features.home.HomeModule
|
import im.vector.app.features.home.HomeModule
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
||||||
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||||
|
import im.vector.app.features.home.room.detail.search.SearchActivity
|
||||||
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
||||||
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
|
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
|
||||||
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
||||||
@ -142,6 +143,7 @@ interface ScreenComponent {
|
|||||||
fun inject(activity: VectorCallActivity)
|
fun inject(activity: VectorCallActivity)
|
||||||
fun inject(activity: VectorAttachmentViewerActivity)
|
fun inject(activity: VectorAttachmentViewerActivity)
|
||||||
fun inject(activity: VectorJitsiActivity)
|
fun inject(activity: VectorJitsiActivity)
|
||||||
|
fun inject(activity: SearchActivity)
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* BottomSheets
|
* BottomSheets
|
||||||
|
@ -673,10 +673,22 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
roomDetailViewModel.handle(RoomDetailAction.EndCall)
|
roomDetailViewModel.handle(RoomDetailAction.EndCall)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.search -> {
|
||||||
|
handleSearchAction()
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSearchAction() {
|
||||||
|
if (session.getRoom(roomDetailArgs.roomId)?.isEncrypted() == false) {
|
||||||
|
navigator.openSearch(requireContext(), roomDetailArgs.roomId)
|
||||||
|
} else {
|
||||||
|
showDialogWithMessage(getString(R.string.search_is_not_supported_in_e2e_room))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleCallRequest(item: MenuItem) = withState(roomDetailViewModel) { state ->
|
private fun handleCallRequest(item: MenuItem) = withState(roomDetailViewModel) { state ->
|
||||||
val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
|
val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
|
||||||
val isVideoCall = item.itemId == R.id.video_call
|
val isVideoCall = item.itemId == R.id.video_call
|
||||||
|
@ -538,6 +538,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
R.id.voice_call,
|
R.id.voice_call,
|
||||||
R.id.video_call -> true // always show for discoverability
|
R.id.video_call -> true // always show for discoverability
|
||||||
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
|
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
|
||||||
|
R.id.search -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.search
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class SearchAction : VectorViewModelAction {
|
||||||
|
data class SearchWith(val searchTerm: String) : SearchAction()
|
||||||
|
object LoadMore : SearchAction()
|
||||||
|
object Retry : SearchAction()
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.search
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.di.ScreenComponent
|
||||||
|
import im.vector.app.core.extensions.addFragment
|
||||||
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
|
import kotlinx.android.synthetic.main.activity_search.*
|
||||||
|
|
||||||
|
class SearchActivity : VectorBaseActivity() {
|
||||||
|
|
||||||
|
private val searchFragment: SearchFragment?
|
||||||
|
get() {
|
||||||
|
return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? SearchFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLayoutRes() = R.layout.activity_search
|
||||||
|
|
||||||
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
super.injectWith(injector)
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
configureToolbar(searchToolbar)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initUiAndData() {
|
||||||
|
if (isFirstCreation()) {
|
||||||
|
val fragmentArgs: SearchArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return
|
||||||
|
addFragment(R.id.searchFragmentContainer, SearchFragment::class.java, fragmentArgs, FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
|
searchFragment?.search(query)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Open the keyboard immediately
|
||||||
|
searchView.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FRAGMENT_TAG = "SearchFragment"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, args: SearchArgs): Intent {
|
||||||
|
return Intent(context, SearchActivity::class.java).apply {
|
||||||
|
// If we do that we will have the same room two times on the stack. Let's allow infinite stack for the moment.
|
||||||
|
// flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
||||||
|
putExtra(MvRx.KEY_ARG, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.search
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.args
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.cleanup
|
||||||
|
import im.vector.app.core.extensions.configureWith
|
||||||
|
import im.vector.app.core.extensions.hideKeyboard
|
||||||
|
import im.vector.app.core.extensions.trackItemsVisibilityChange
|
||||||
|
import im.vector.app.core.platform.StateView
|
||||||
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.android.synthetic.main.fragment_search.*
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class SearchArgs(
|
||||||
|
val roomId: String
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
class SearchFragment @Inject constructor(
|
||||||
|
val viewModelFactory: SearchViewModel.Factory,
|
||||||
|
private val controller: SearchResultController
|
||||||
|
) : VectorBaseFragment(), StateView.EventCallback, SearchResultController.Listener {
|
||||||
|
|
||||||
|
private val fragmentArgs: SearchArgs by args()
|
||||||
|
private val searchViewModel: SearchViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
private var pendingScrollToPosition: Int? = null
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_search
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
stateView.contentView = searchResultRecycler
|
||||||
|
stateView.eventCallback = this
|
||||||
|
|
||||||
|
configureRecyclerView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureRecyclerView() {
|
||||||
|
searchResultRecycler.trackItemsVisibilityChange()
|
||||||
|
searchResultRecycler.configureWith(controller, showDivider = false)
|
||||||
|
(searchResultRecycler.layoutManager as? LinearLayoutManager)?.stackFromEnd = true
|
||||||
|
controller.listener = this
|
||||||
|
|
||||||
|
controller.addModelBuildListener {
|
||||||
|
pendingScrollToPosition?.let {
|
||||||
|
searchResultRecycler.smoothScrollToPosition(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
searchResultRecycler?.cleanup()
|
||||||
|
controller.listener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(searchViewModel) { state ->
|
||||||
|
if (state.searchResult.isNullOrEmpty()) {
|
||||||
|
when (state.asyncSearchRequest) {
|
||||||
|
is Loading -> {
|
||||||
|
stateView.state = StateView.State.Loading
|
||||||
|
}
|
||||||
|
is Fail -> {
|
||||||
|
stateView.state = StateView.State.Error(errorFormatter.toHumanReadable(state.asyncSearchRequest.error))
|
||||||
|
}
|
||||||
|
is Success -> {
|
||||||
|
stateView.state = StateView.State.Empty(
|
||||||
|
title = getString(R.string.search_no_results),
|
||||||
|
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_search_no_results))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pendingScrollToPosition = (state.lastBatchSize - 1).coerceAtLeast(0)
|
||||||
|
|
||||||
|
stateView.state = StateView.State.Content
|
||||||
|
controller.setData(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String) {
|
||||||
|
view?.hideKeyboard()
|
||||||
|
searchViewModel.handle(SearchAction.SearchWith(query))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRetryClicked() {
|
||||||
|
searchViewModel.handle(SearchAction.Retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClicked(event: Event) {
|
||||||
|
event.roomId?.let {
|
||||||
|
navigator.openRoom(requireContext(), it, event.eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadMore() {
|
||||||
|
searchViewModel.handle(SearchAction.LoadMore)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.search
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import com.airbnb.epoxy.VisibilityState
|
||||||
|
import im.vector.app.core.date.DateFormatKind
|
||||||
|
import im.vector.app.core.date.VectorDateFormatter
|
||||||
|
import im.vector.app.core.epoxy.loadingItem
|
||||||
|
import im.vector.app.core.ui.list.genericItemHeader
|
||||||
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.search.EventAndSender
|
||||||
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
import java.util.Calendar
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SearchResultController @Inject constructor(
|
||||||
|
private val session: Session,
|
||||||
|
private val avatarRenderer: AvatarRenderer,
|
||||||
|
private val dateFormatter: VectorDateFormatter
|
||||||
|
) : TypedEpoxyController<SearchViewState>() {
|
||||||
|
|
||||||
|
var listener: Listener? = null
|
||||||
|
|
||||||
|
private var idx = 0
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onItemClicked(event: Event)
|
||||||
|
fun loadMore()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
setData(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildModels(data: SearchViewState?) {
|
||||||
|
data ?: return
|
||||||
|
|
||||||
|
if (data.hasMoreResult) {
|
||||||
|
loadingItem {
|
||||||
|
// Always use a different id, because we can be notified several times of visibility state changed
|
||||||
|
id("loadMore${idx++}")
|
||||||
|
onVisibilityStateChanged { _, _, visibilityState ->
|
||||||
|
if (visibilityState == VisibilityState.VISIBLE) {
|
||||||
|
listener?.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSearchResultItems(data.searchResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSearchResultItems(events: List<EventAndSender>) {
|
||||||
|
var lastDate: Calendar? = null
|
||||||
|
|
||||||
|
events.forEach { eventAndSender ->
|
||||||
|
val eventDate = Calendar.getInstance().apply {
|
||||||
|
timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
if (lastDate?.get(Calendar.DAY_OF_YEAR) != eventDate.get(Calendar.DAY_OF_YEAR)) {
|
||||||
|
genericItemHeader {
|
||||||
|
id(eventDate.hashCode())
|
||||||
|
text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastDate = eventDate
|
||||||
|
|
||||||
|
searchResultItem {
|
||||||
|
id(eventAndSender.event.eventId)
|
||||||
|
avatarRenderer(avatarRenderer)
|
||||||
|
dateFormatter(dateFormatter)
|
||||||
|
event(eventAndSender.event)
|
||||||
|
sender(eventAndSender.sender
|
||||||
|
?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
|
||||||
|
listener { listener?.onItemClicked(eventAndSender.event) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.search
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.date.DateFormatKind
|
||||||
|
import im.vector.app.core.date.VectorDateFormatter
|
||||||
|
import im.vector.app.core.epoxy.ClickListener
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.app.core.epoxy.onClick
|
||||||
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_search_result)
|
||||||
|
abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
@EpoxyAttribute var dateFormatter: VectorDateFormatter? = null
|
||||||
|
@EpoxyAttribute lateinit var event: Event
|
||||||
|
@EpoxyAttribute var sender: MatrixItem? = null
|
||||||
|
@EpoxyAttribute var listener: ClickListener? = null
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
|
||||||
|
holder.view.onClick(listener)
|
||||||
|
sender?.let { avatarRenderer.render(it, holder.avatarImageView) }
|
||||||
|
holder.memberNameView.setTextOrHide(sender?.getBestName())
|
||||||
|
holder.timeView.text = dateFormatter?.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
|
||||||
|
// TODO Improve that (use formattedBody, etc.)
|
||||||
|
holder.contentView.text = event.content?.get("body") as? String
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||||
|
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
|
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
|
val contentView by bind<TextView>(R.id.messageContentView)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.search
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
|
||||||
|
sealed class SearchViewEvents : VectorViewEvents {
|
||||||
|
data class Failure(val throwable: Throwable) : SearchViewEvents()
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.search
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.extensions.exhaustive
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.room.Room
|
||||||
|
import org.matrix.android.sdk.api.session.search.SearchResult
|
||||||
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
|
|
||||||
|
class SearchViewModel @AssistedInject constructor(
|
||||||
|
@Assisted private val initialState: SearchViewState,
|
||||||
|
session: Session
|
||||||
|
) : VectorViewModel<SearchViewState, SearchAction, SearchViewEvents>(initialState) {
|
||||||
|
|
||||||
|
private var room: Room? = session.getRoom(initialState.roomId)
|
||||||
|
|
||||||
|
private var currentTask: Cancelable? = null
|
||||||
|
|
||||||
|
private var nextBatch: String? = null
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: SearchViewState): SearchViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<SearchViewModel, SearchViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: SearchViewState): SearchViewModel? {
|
||||||
|
val fragment: SearchFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
return fragment.viewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: SearchAction) {
|
||||||
|
when (action) {
|
||||||
|
is SearchAction.SearchWith -> handleSearchWith(action)
|
||||||
|
is SearchAction.LoadMore -> handleLoadMore()
|
||||||
|
is SearchAction.Retry -> handleRetry()
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSearchWith(action: SearchAction.SearchWith) {
|
||||||
|
if (action.searchTerm.isNotEmpty()) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
searchResult = emptyList(),
|
||||||
|
hasMoreResult = false,
|
||||||
|
lastBatchSize = 0,
|
||||||
|
searchTerm = action.searchTerm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
startSearching(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleLoadMore() {
|
||||||
|
startSearching(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRetry() {
|
||||||
|
startSearching(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startSearching(isNextBatch: Boolean) = withState { state ->
|
||||||
|
if (state.searchTerm == null) return@withState
|
||||||
|
|
||||||
|
// There is no batch to retrieve
|
||||||
|
if (isNextBatch && nextBatch == null) return@withState
|
||||||
|
|
||||||
|
// Show full screen loading just for the clean search
|
||||||
|
if (!isNextBatch) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
asyncSearchRequest = Loading()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTask?.cancel()
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val result = awaitCallback<SearchResult> {
|
||||||
|
currentTask = room?.search(
|
||||||
|
searchTerm = state.searchTerm,
|
||||||
|
nextBatch = nextBatch,
|
||||||
|
orderByRecent = true,
|
||||||
|
beforeLimit = 0,
|
||||||
|
afterLimit = 0,
|
||||||
|
includeProfile = true,
|
||||||
|
limit = 20,
|
||||||
|
callback = it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onSearchResultSuccess(result)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
if (failure is Failure.Cancelled) return@launch
|
||||||
|
|
||||||
|
_viewEvents.post(SearchViewEvents.Failure(failure))
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
asyncSearchRequest = Fail(failure)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSearchResultSuccess(searchResult: SearchResult) = withState { state ->
|
||||||
|
val accumulatedResult = searchResult.results.orEmpty().plus(state.searchResult)
|
||||||
|
|
||||||
|
// Note: We do not care about the highlights for the moment, but it will be the same algorithm
|
||||||
|
|
||||||
|
nextBatch = searchResult.nextBatch
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
searchResult = accumulatedResult,
|
||||||
|
hasMoreResult = !nextBatch.isNullOrEmpty(),
|
||||||
|
lastBatchSize = searchResult.results.orEmpty().size,
|
||||||
|
asyncSearchRequest = Success(Unit)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
currentTask?.cancel()
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.search
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import org.matrix.android.sdk.api.session.search.EventAndSender
|
||||||
|
|
||||||
|
data class SearchViewState(
|
||||||
|
// Accumulated search result
|
||||||
|
val searchResult: List<EventAndSender> = emptyList(),
|
||||||
|
val hasMoreResult: Boolean = false,
|
||||||
|
// Last batch size, will help RecyclerView to position itself
|
||||||
|
val lastBatchSize: Int = 0,
|
||||||
|
val searchTerm: String? = null,
|
||||||
|
val roomId: String = "",
|
||||||
|
// Current pagination request
|
||||||
|
val asyncSearchRequest: Async<Unit> = Uninitialized
|
||||||
|
) : MvRxState {
|
||||||
|
|
||||||
|
constructor(args: SearchArgs) : this(roomId = args.roomId)
|
||||||
|
}
|
@ -43,6 +43,8 @@ import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
|||||||
import im.vector.app.features.debug.DebugMenuActivity
|
import im.vector.app.features.debug.DebugMenuActivity
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailArgs
|
import im.vector.app.features.home.room.detail.RoomDetailArgs
|
||||||
|
import im.vector.app.features.home.room.detail.search.SearchActivity
|
||||||
|
import im.vector.app.features.home.room.detail.search.SearchArgs
|
||||||
import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes
|
import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes
|
||||||
import im.vector.app.features.home.room.filtered.FilteredRoomsActivity
|
import im.vector.app.features.home.room.filtered.FilteredRoomsActivity
|
||||||
import im.vector.app.features.invite.InviteUsersToRoomActivity
|
import im.vector.app.features.invite.InviteUsersToRoomActivity
|
||||||
@ -329,6 +331,11 @@ class DefaultNavigator @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun openSearch(context: Context, roomId: String) {
|
||||||
|
val intent = SearchActivity.newIntent(context, SearchArgs(roomId))
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
|
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
|
||||||
if (buildTask) {
|
if (buildTask) {
|
||||||
val stackBuilder = TaskStackBuilder.create(context)
|
val stackBuilder = TaskStackBuilder.create(context)
|
||||||
|
@ -109,4 +109,6 @@ interface Navigator {
|
|||||||
view: View,
|
view: View,
|
||||||
inMemory: List<AttachmentData> = emptyList(),
|
inMemory: List<AttachmentData> = emptyList(),
|
||||||
options: ((MutableList<Pair<View, String>>) -> Unit)?)
|
options: ((MutableList<Pair<View, String>>) -> Unit)?)
|
||||||
|
|
||||||
|
fun openSearch(context: Context, roomId: String)
|
||||||
}
|
}
|
||||||
|
@ -23,23 +23,21 @@ import com.airbnb.mvrx.args
|
|||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.core.resources.StringProvider
|
|
||||||
import im.vector.app.core.utils.saveMedia
|
import im.vector.app.core.utils.saveMedia
|
||||||
import im.vector.app.core.utils.shareMedia
|
import im.vector.app.core.utils.shareMedia
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.notifications.NotificationUtils
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
import im.vector.app.features.roomprofile.RoomProfileArgs
|
import im.vector.app.features.roomprofile.RoomProfileArgs
|
||||||
import kotlinx.android.synthetic.main.fragment_room_uploads.*
|
import kotlinx.android.synthetic.main.fragment_room_uploads.*
|
||||||
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomUploadsFragment @Inject constructor(
|
class RoomUploadsFragment @Inject constructor(
|
||||||
private val viewModelFactory: RoomUploadsViewModel.Factory,
|
private val viewModelFactory: RoomUploadsViewModel.Factory,
|
||||||
private val stringProvider: StringProvider,
|
|
||||||
private val avatarRenderer: AvatarRenderer,
|
private val avatarRenderer: AvatarRenderer,
|
||||||
private val notificationUtils: NotificationUtils
|
private val notificationUtils: NotificationUtils
|
||||||
) : VectorBaseFragment(), RoomUploadsViewModel.Factory by viewModelFactory {
|
) : VectorBaseFragment(), RoomUploadsViewModel.Factory by viewModelFactory {
|
||||||
@ -58,8 +56,8 @@ class RoomUploadsFragment @Inject constructor(
|
|||||||
|
|
||||||
TabLayoutMediator(roomUploadsTabs, roomUploadsViewPager) { tab, position ->
|
TabLayoutMediator(roomUploadsTabs, roomUploadsViewPager) { tab, position ->
|
||||||
when (position) {
|
when (position) {
|
||||||
0 -> tab.text = stringProvider.getString(R.string.uploads_media_title)
|
0 -> tab.text = getString(R.string.uploads_media_title)
|
||||||
1 -> tab.text = stringProvider.getString(R.string.uploads_files_title)
|
1 -> tab.text = getString(R.string.uploads_files_title)
|
||||||
}
|
}
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
@ -70,7 +68,7 @@ class RoomUploadsFragment @Inject constructor(
|
|||||||
is RoomUploadsViewEvents.FileReadyForSharing -> {
|
is RoomUploadsViewEvents.FileReadyForSharing -> {
|
||||||
shareMedia(requireContext(), it.file, getMimeTypeFromUri(requireContext(), it.file.toUri()))
|
shareMedia(requireContext(), it.file, getMimeTypeFromUri(requireContext(), it.file.toUri()))
|
||||||
}
|
}
|
||||||
is RoomUploadsViewEvents.FileReadyForSaving -> {
|
is RoomUploadsViewEvents.FileReadyForSaving -> {
|
||||||
saveMedia(
|
saveMedia(
|
||||||
context = requireContext(),
|
context = requireContext(),
|
||||||
file = it.file,
|
file = it.file,
|
||||||
@ -79,7 +77,7 @@ class RoomUploadsFragment @Inject constructor(
|
|||||||
notificationUtils = notificationUtils
|
notificationUtils = notificationUtils
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is RoomUploadsViewEvents.Failure -> showFailure(it.throwable)
|
is RoomUploadsViewEvents.Failure -> showFailure(it.throwable)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
vector/src/main/res/drawable/ic_search_no_results.xml
Normal file
20
vector/src/main/res/drawable/ic_search_no_results.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape
|
||||||
|
android:innerRadius="0dp"
|
||||||
|
android:shape="ring"
|
||||||
|
android:thicknessRatio="2"
|
||||||
|
android:useLevel="false">
|
||||||
|
<solid android:color="@android:color/transparent" />
|
||||||
|
<stroke android:width="2dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:bottom="14dp"
|
||||||
|
android:drawable="@drawable/ic_search"
|
||||||
|
android:left="14dp"
|
||||||
|
android:right="14dp"
|
||||||
|
android:top="14dp" />
|
||||||
|
</layer-list>
|
43
vector/src/main/res/layout/activity_search.xml
Normal file
43
vector/src/main/res/layout/activity_search.xml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/searchToolbar"
|
||||||
|
style="@style/VectorToolbarStyle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:elevation="4dp"
|
||||||
|
app:contentInsetStart="0dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SearchView
|
||||||
|
android:id="@+id/searchView"
|
||||||
|
style="@style/VectorSearchView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:backgroundTint="@color/base_color"
|
||||||
|
app:queryHint="@string/search_hint" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/searchFragmentContainer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/searchToolbar" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
15
vector/src/main/res/layout/fragment_search.xml
Normal file
15
vector/src/main/res/layout/fragment_search.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<im.vector.app.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/stateView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/searchResultRecycler"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:overScrollMode="always"
|
||||||
|
tools:listitem="@layout/item_search_result" />
|
||||||
|
|
||||||
|
</im.vector.app.core.platform.StateView>
|
62
vector/src/main/res/layout/item_search_result.xml
Normal file
62
vector/src/main/res/layout/item_search_result.xml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?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:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/messageAvatarImageView"
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageMemberNameView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/messageAvatarImageView"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@sample/matrix.json/data/displayName" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageTimeView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignBaseline="@+id/messageMemberNameView"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/messageMemberNameView"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:text="@tools:sample/date/hhmm" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageContentView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/messageMemberNameView"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView"
|
||||||
|
tools:text="@sample/matrix.json/data/message" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -3,6 +3,11 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/search"
|
||||||
|
android:title="@string/search"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/video_call"
|
android:id="@+id/video_call"
|
||||||
android:icon="@drawable/ic_video"
|
android:icon="@drawable/ic_video"
|
||||||
|
@ -628,6 +628,7 @@
|
|||||||
<string name="tab_title_search_messages">MESSAGES</string>
|
<string name="tab_title_search_messages">MESSAGES</string>
|
||||||
<string name="tab_title_search_people">PEOPLE</string>
|
<string name="tab_title_search_people">PEOPLE</string>
|
||||||
<string name="tab_title_search_files">FILES</string>
|
<string name="tab_title_search_files">FILES</string>
|
||||||
|
<string name="search_is_not_supported_in_e2e_room">Searching in encrypted rooms is not supported yet.</string>
|
||||||
|
|
||||||
<!-- Room recents -->
|
<!-- Room recents -->
|
||||||
<string name="room_recents_join">JOIN</string>
|
<string name="room_recents_join">JOIN</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user