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 ✨:
|
||||
- Search messages in a room - phase 1 (#2110)
|
||||
- Hide encrypted history (before user is invited). Can be shown if wanted in developer settings
|
||||
|
||||
Improvements 🙌:
|
||||
@ -19,7 +20,7 @@ Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
-
|
||||
- Search messages in a room by using Session.searchService() or Room.search()
|
||||
|
||||
Build 🧱:
|
||||
- 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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.matrix.android.sdk">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@ -7,6 +8,15 @@
|
||||
|
||||
<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.
|
||||
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.room.RoomDirectoryService
|
||||
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.SharedSecretStorageService
|
||||
import org.matrix.android.sdk.api.session.signout.SignOutService
|
||||
@ -201,6 +202,11 @@ interface Session :
|
||||
*/
|
||||
fun permalinkService(): PermalinkService
|
||||
|
||||
/**
|
||||
* Returns the search service associated with the session
|
||||
*/
|
||||
fun searchService(): SearchService
|
||||
|
||||
/**
|
||||
* Add a listener to the session.
|
||||
* @param listener the listener to add.
|
||||
|
@ -18,6 +18,7 @@
|
||||
package org.matrix.android.sdk.api.session.room
|
||||
|
||||
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.crypto.RoomCryptoService
|
||||
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.typing.TypingService
|
||||
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
|
||||
|
||||
/**
|
||||
@ -69,4 +72,25 @@ interface Room :
|
||||
* A current snapshot of [RoomSummary] associated with the room
|
||||
*/
|
||||
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.room.RoomDirectoryService
|
||||
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.SharedSecretStorageService
|
||||
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 pushersService: Lazy<PushersService>,
|
||||
private val termsService: Lazy<TermsService>,
|
||||
private val searchService: Lazy<SearchService>,
|
||||
private val cryptoService: Lazy<DefaultCryptoService>,
|
||||
private val defaultFileService: Lazy<FileService>,
|
||||
private val permalinkService: Lazy<PermalinkService>,
|
||||
@ -264,6 +266,8 @@ internal class DefaultSession @Inject constructor(
|
||||
|
||||
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
||||
|
||||
override fun searchService(): SearchService = searchService.get()
|
||||
|
||||
override fun getOkHttpClient(): OkHttpClient {
|
||||
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.RedactEventWorker
|
||||
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.sync.SyncModule
|
||||
import org.matrix.android.sdk.internal.session.sync.SyncTask
|
||||
@ -86,7 +87,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
ProfileModule::class,
|
||||
SessionAssistedInjectModule::class,
|
||||
AccountModule::class,
|
||||
CallModule::class
|
||||
CallModule::class,
|
||||
SearchModule::class
|
||||
]
|
||||
)
|
||||
@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.typing.TypingService
|
||||
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.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
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.search.SearchTask
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
import java.security.InvalidParameterException
|
||||
@ -62,7 +65,8 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
||||
private val roomMembersService: MembershipService,
|
||||
private val roomPushRuleService: RoomPushRuleService,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val sendStateTask: SendStateTask) :
|
||||
private val sendStateTask: SendStateTask,
|
||||
private val searchTask: SearchTask) :
|
||||
Room,
|
||||
TimelineService by timelineService,
|
||||
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.typing.DefaultTypingService
|
||||
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 javax.inject.Inject
|
||||
|
||||
@ -59,7 +60,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
|
||||
private val membershipServiceFactory: DefaultMembershipService.Factory,
|
||||
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val sendStateTask: SendStateTask) :
|
||||
private val sendStateTask: SendStateTask,
|
||||
private val searchTask: SearchTask) :
|
||||
RoomFactory {
|
||||
|
||||
override fun create(roomId: String): Room {
|
||||
@ -81,7 +83,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
|
||||
roomMembersService = membershipServiceFactory.create(roomId),
|
||||
roomPushRuleService = roomPushRuleServiceFactory.create(roomId),
|
||||
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.widgets.WidgetActivity" />
|
||||
<activity android:name=".features.pin.PinActivity" />
|
||||
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
||||
|
||||
<!-- 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.room.breadcrumbs.BreadcrumbsFragment
|
||||
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.login.LoginCaptchaFragment
|
||||
import im.vector.app.features.login.LoginFragment
|
||||
@ -570,4 +571,9 @@ interface FragmentModule {
|
||||
@IntoMap
|
||||
@FragmentKey(RoomBannedMemberListFragment::class)
|
||||
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.room.detail.RoomDetailActivity
|
||||
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.edithistory.ViewEditHistoryBottomSheet
|
||||
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
||||
@ -142,6 +143,7 @@ interface ScreenComponent {
|
||||
fun inject(activity: VectorCallActivity)
|
||||
fun inject(activity: VectorAttachmentViewerActivity)
|
||||
fun inject(activity: VectorJitsiActivity)
|
||||
fun inject(activity: SearchActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
@ -673,10 +673,22 @@ class RoomDetailFragment @Inject constructor(
|
||||
roomDetailViewModel.handle(RoomDetailAction.EndCall)
|
||||
true
|
||||
}
|
||||
R.id.search -> {
|
||||
handleSearchAction()
|
||||
true
|
||||
}
|
||||
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 ->
|
||||
val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
|
||||
val isVideoCall = item.itemId == R.id.video_call
|
||||
|
@ -538,6 +538,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
R.id.voice_call,
|
||||
R.id.video_call -> true // always show for discoverability
|
||||
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
|
||||
R.id.search -> true
|
||||
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.home.room.detail.RoomDetailActivity
|
||||
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.filtered.FilteredRoomsActivity
|
||||
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) {
|
||||
if (buildTask) {
|
||||
val stackBuilder = TaskStackBuilder.create(context)
|
||||
|
@ -109,4 +109,6 @@ interface Navigator {
|
||||
view: View,
|
||||
inMemory: List<AttachmentData> = emptyList(),
|
||||
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.withState
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||
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.shareMedia
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import im.vector.app.features.roomprofile.RoomProfileArgs
|
||||
import kotlinx.android.synthetic.main.fragment_room_uploads.*
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomUploadsFragment @Inject constructor(
|
||||
private val viewModelFactory: RoomUploadsViewModel.Factory,
|
||||
private val stringProvider: StringProvider,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val notificationUtils: NotificationUtils
|
||||
) : VectorBaseFragment(), RoomUploadsViewModel.Factory by viewModelFactory {
|
||||
@ -58,8 +56,8 @@ class RoomUploadsFragment @Inject constructor(
|
||||
|
||||
TabLayoutMediator(roomUploadsTabs, roomUploadsViewPager) { tab, position ->
|
||||
when (position) {
|
||||
0 -> tab.text = stringProvider.getString(R.string.uploads_media_title)
|
||||
1 -> tab.text = stringProvider.getString(R.string.uploads_files_title)
|
||||
0 -> tab.text = getString(R.string.uploads_media_title)
|
||||
1 -> tab.text = getString(R.string.uploads_files_title)
|
||||
}
|
||||
}.attach()
|
||||
|
||||
|
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:tools="http://schemas.android.com/tools">
|
||||
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:title="@string/search"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/video_call"
|
||||
android:icon="@drawable/ic_video"
|
||||
|
@ -628,6 +628,7 @@
|
||||
<string name="tab_title_search_messages">MESSAGES</string>
|
||||
<string name="tab_title_search_people">PEOPLE</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 -->
|
||||
<string name="room_recents_join">JOIN</string>
|
||||
|
Loading…
Reference in New Issue
Block a user