Merge branch 'develop' into feature/ons/fix_email_confirmation_flow
This commit is contained in:
commit
1f4a360a0c
|
@ -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 🙌:
|
||||
|
@ -11,6 +12,7 @@ Improvements 🙌:
|
|||
- Allow user to reset cross signing if he has no way to recover (#2052)
|
||||
- Create home shortcut for any room (#1525)
|
||||
- Can't confirm email due to killing by Android (#2021)
|
||||
- Filter room member (and banned users) by name (#2184)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Improve support for image/audio/video/file selection with intent changes (#1376)
|
||||
|
@ -20,7 +22,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.event.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.event.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,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
|
||||
|
||||
@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
|
||||
)
|
|
@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
|
|||
# android\.text\.TextUtils
|
||||
|
||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt
|
||||
enum class===80
|
||||
enum class===81
|
||||
|
||||
### Do not import temporary legacy classes
|
||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -25,7 +25,7 @@ class RoomListNameFilter @Inject constructor() : Predicate<RoomSummary> {
|
|||
var filter: String = ""
|
||||
|
||||
override fun test(roomSummary: RoomSummary): Boolean {
|
||||
if (filter.isBlank()) {
|
||||
if (filter.isEmpty()) {
|
||||
// No filter
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -22,4 +22,5 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||
sealed class RoomBannedListMemberAction : VectorViewModelAction {
|
||||
data class QueryInfo(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction()
|
||||
data class UnBanUser(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction()
|
||||
data class Filter(val filter: String) : RoomBannedListMemberAction()
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.util.awaitCallback
|
|||
import org.matrix.android.sdk.rx.rx
|
||||
import org.matrix.android.sdk.rx.unwrap
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
|
@ -90,6 +91,15 @@ class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initia
|
|||
when (action) {
|
||||
is RoomBannedListMemberAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary)
|
||||
is RoomBannedListMemberAction.UnBanUser -> unBanUser(action.roomMemberSummary)
|
||||
is RoomBannedListMemberAction.Filter -> handleFilter(action)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleFilter(action: RoomBannedListMemberAction.Filter) {
|
||||
setState {
|
||||
copy(
|
||||
filter = action.filter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
package im.vector.app.features.roomprofile.banned
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.dividerItem
|
||||
import im.vector.app.core.epoxy.profiles.buildProfileSection
|
||||
|
@ -28,11 +26,15 @@ import im.vector.app.core.resources.ColorProvider
|
|||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.ui.list.genericFooterItem
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.roomprofile.members.RoomMemberSummaryFilter
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomBannedMemberListController @Inject constructor(
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val stringProvider: StringProvider,
|
||||
private val roomMemberSummaryFilter: RoomMemberSummaryFilter,
|
||||
colorProvider: ColorProvider
|
||||
) : TypedEpoxyController<RoomBannedMemberListViewState>() {
|
||||
|
||||
|
@ -63,7 +65,10 @@ class RoomBannedMemberListController @Inject constructor(
|
|||
} else {
|
||||
buildProfileSection(quantityString)
|
||||
|
||||
bannedList.join(
|
||||
roomMemberSummaryFilter.filter = data.filter
|
||||
bannedList
|
||||
.filter { roomMemberSummaryFilter.test(it) }
|
||||
.join(
|
||||
each = { _, roomMember ->
|
||||
val actionInProgress = data.onGoingModerationAction.contains(roomMember.userId)
|
||||
profileMatrixItemWithProgress {
|
||||
|
|
|
@ -19,6 +19,8 @@ package im.vector.app.features.roomprofile.banned
|
|||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
|
@ -53,6 +55,7 @@ class RoomBannedMemberListFragment @Inject constructor(
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
roomMemberListController.callback = this
|
||||
setupToolbar(roomSettingsToolbar)
|
||||
setupSearchView()
|
||||
recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
|
||||
|
||||
viewModel.observeViewEvents {
|
||||
|
@ -84,6 +87,21 @@ class RoomBannedMemberListFragment @Inject constructor(
|
|||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupSearchView() {
|
||||
searchViewAppBarLayout.isVisible = true
|
||||
searchView.queryHint = getString(R.string.search_banned_user_hint)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
viewModel.handle(RoomBannedListMemberAction.Filter(newText))
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { viewState ->
|
||||
roomMemberListController.setData(viewState)
|
||||
renderRoomSummary(viewState)
|
||||
|
|
|
@ -27,6 +27,7 @@ data class RoomBannedMemberListViewState(
|
|||
val roomId: String,
|
||||
val roomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val bannedMemberSummaries: Async<List<RoomMemberSummary>> = Uninitialized,
|
||||
val filter: String = "",
|
||||
val onGoingModerationAction: List<String> = emptyList(),
|
||||
val canUserBan: Boolean = false
|
||||
) : MvRxState {
|
||||
|
|
|
@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||
|
||||
sealed class RoomMemberListAction : VectorViewModelAction {
|
||||
data class RevokeThreePidInvite(val stateKey: String) : RoomMemberListAction()
|
||||
data class FilterMemberList(val searchTerm: String) : RoomMemberListAction()
|
||||
}
|
||||
|
|
|
@ -17,12 +17,6 @@
|
|||
package im.vector.app.features.roomprofile.members
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.dividerItem
|
||||
import im.vector.app.core.epoxy.profiles.buildProfileSection
|
||||
|
@ -31,17 +25,24 @@ import im.vector.app.core.extensions.join
|
|||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomMemberListController @Inject constructor(
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val stringProvider: StringProvider,
|
||||
private val roomMemberSummaryFilter: RoomMemberSummaryFilter,
|
||||
colorProvider: ColorProvider
|
||||
) : TypedEpoxyController<RoomMemberListViewState>() {
|
||||
|
||||
interface Callback {
|
||||
fun onRoomMemberClicked(roomMember: RoomMemberSummary)
|
||||
fun onThreePidInvites(event: Event)
|
||||
fun onThreePidInviteClicked(event: Event)
|
||||
}
|
||||
|
||||
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
|
||||
|
@ -53,17 +54,29 @@ class RoomMemberListController @Inject constructor(
|
|||
}
|
||||
|
||||
override fun buildModels(data: RoomMemberListViewState?) {
|
||||
val roomMembersByPowerLevel = data?.roomMemberSummaries?.invoke() ?: return
|
||||
val threePidInvites = data.threePidInvites().orEmpty()
|
||||
data ?: return
|
||||
|
||||
roomMemberSummaryFilter.filter = data.filter
|
||||
|
||||
val roomMembersByPowerLevel = data.roomMemberSummaries.invoke() ?: return
|
||||
val threePidInvites = data.threePidInvites()
|
||||
?.filter { event ->
|
||||
event.content.toModel<RoomThirdPartyInviteContent>()
|
||||
?.takeIf {
|
||||
data.filter.isEmpty() || it.displayName.contains(data.filter, ignoreCase = true)
|
||||
} != null
|
||||
}
|
||||
.orEmpty()
|
||||
var threePidInvitesDone = threePidInvites.isEmpty()
|
||||
|
||||
for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) {
|
||||
if (roomMemberList.isEmpty()) {
|
||||
val filteredRoomMemberList = roomMemberList.filter { roomMemberSummaryFilter.test(it) }
|
||||
if (filteredRoomMemberList.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (powerLevelCategory == RoomMemberListCategories.USER && !threePidInvitesDone) {
|
||||
// If there is not regular invite, display threepid invite before the regular user
|
||||
// If there is no regular invite, display threepid invite before the regular user
|
||||
buildProfileSection(
|
||||
stringProvider.getString(RoomMemberListCategories.INVITE.titleRes)
|
||||
)
|
||||
|
@ -75,7 +88,7 @@ class RoomMemberListController @Inject constructor(
|
|||
buildProfileSection(
|
||||
stringProvider.getString(powerLevelCategory.titleRes)
|
||||
)
|
||||
roomMemberList.join(
|
||||
filteredRoomMemberList.join(
|
||||
each = { _, roomMember ->
|
||||
profileMatrixItem {
|
||||
id(roomMember.userId)
|
||||
|
@ -94,12 +107,13 @@ class RoomMemberListController @Inject constructor(
|
|||
}
|
||||
}
|
||||
)
|
||||
if (powerLevelCategory == RoomMemberListCategories.INVITE) {
|
||||
if (powerLevelCategory == RoomMemberListCategories.INVITE && !threePidInvitesDone) {
|
||||
// Display the threepid invite after the regular invite
|
||||
dividerItem {
|
||||
id("divider_threepidinvites")
|
||||
color(dividerColor)
|
||||
}
|
||||
|
||||
buildThreePidInvites(data)
|
||||
threePidInvitesDone = true
|
||||
}
|
||||
|
@ -128,7 +142,7 @@ class RoomMemberListController @Inject constructor(
|
|||
avatarRenderer(avatarRenderer)
|
||||
editable(data.actionsPermissions.canRevokeThreePidInvite)
|
||||
clickListener { _ ->
|
||||
callback?.onThreePidInvites(event)
|
||||
callback?.onThreePidInviteClicked(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ import android.view.Menu
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
|
@ -72,12 +74,28 @@ class RoomMemberListFragment @Inject constructor(
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
roomMemberListController.callback = this
|
||||
setupToolbar(roomSettingsToolbar)
|
||||
setupSearchView()
|
||||
recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
|
||||
viewModel.selectSubscribe(this, RoomMemberListViewState::actionsPermissions) {
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSearchView() {
|
||||
searchViewAppBarLayout.isVisible = true
|
||||
searchView.queryHint = getString(R.string.search_members_hint)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
viewModel.handle(RoomMemberListAction.FilterMemberList(newText))
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
recyclerView.cleanup()
|
||||
super.onDestroyView()
|
||||
|
@ -92,7 +110,7 @@ class RoomMemberListFragment @Inject constructor(
|
|||
navigator.openRoomMemberProfile(roomMember.userId, roomId = roomProfileArgs.roomId, context = requireActivity())
|
||||
}
|
||||
|
||||
override fun onThreePidInvites(event: Event) {
|
||||
override fun onThreePidInviteClicked(event: Event) {
|
||||
// Display a dialog to revoke invite if power level is high enough
|
||||
val content = event.content.toModel<RoomThirdPartyInviteContent>() ?: return
|
||||
val stateKey = event.stateKey ?: return
|
||||
|
|
|
@ -188,6 +188,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
|
|||
override fun handle(action: RoomMemberListAction) {
|
||||
when (action) {
|
||||
is RoomMemberListAction.RevokeThreePidInvite -> handleRevokeThreePidInvite(action)
|
||||
is RoomMemberListAction.FilterMemberList -> handleFilterMemberList(action)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -201,4 +202,12 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFilterMemberList(action: RoomMemberListAction.FilterMemberList) {
|
||||
setState {
|
||||
copy(
|
||||
filter = action.searchTerm
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ data class RoomMemberListViewState(
|
|||
val roomId: String,
|
||||
val roomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized,
|
||||
val filter: String = "",
|
||||
val threePidInvites: Async<List<Event>> = Uninitialized,
|
||||
val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized,
|
||||
val actionsPermissions: ActionPermissions = ActionPermissions()
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 im.vector.app.features.roomprofile.members
|
||||
|
||||
import io.reactivex.functions.Predicate
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomMemberSummaryFilter @Inject constructor() : Predicate<RoomMemberSummary> {
|
||||
var filter: String = ""
|
||||
|
||||
override fun test(roomMemberSummary: RoomMemberSummary): Boolean {
|
||||
if (filter.isEmpty()) {
|
||||
// No filter
|
||||
return true
|
||||
}
|
||||
|
||||
return roomMemberSummary.displayName?.contains(filter, ignoreCase = true).orFalse()
|
||||
// We should maybe exclude the domain from the userId
|
||||
|| roomMemberSummary.userId.contains(filter, ignoreCase = true)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -53,17 +53,54 @@
|
|||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:overScrollMode="always"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/roomSettingsToolbar"
|
||||
app:layout_constraintTop_toBottomOf="@+id/roomSettingsToolbar">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:overScrollMode="always"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:listitem="@layout/item_profile_action" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/searchViewAppBarLayout"
|
||||
style="@style/VectorAppBarLayoutStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="4dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<!-- Use an extra container for the margin to be taken into account by the layout_scrollFlags -->
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|enterAlways|snap">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/searchView"
|
||||
style="@style/VectorSearchView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="@null"
|
||||
android:minHeight="0dp"
|
||||
tools:queryHint="@string/search_hint" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<include layout="@layout/merge_overlay_waiting_view" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -623,11 +623,13 @@
|
|||
<!-- Search -->
|
||||
<string name="search_hint">Search</string>
|
||||
<string name="search_members_hint">Filter room members</string>
|
||||
<string name="search_banned_user_hint">Filter banned users</string>
|
||||
<string name="search_no_results">No results</string>
|
||||
<string name="tab_title_search_rooms">ROOMS</string>
|
||||
<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>
|
||||
|
@ -1850,11 +1852,11 @@
|
|||
<string name="block_user">"IGNORE USER"</string>
|
||||
|
||||
<string name="content_reported_title">"Content reported"</string>
|
||||
<string name="content_reported_content">"This content was reported.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
|
||||
<string name="content_reported_content">"This content was reported.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string>
|
||||
<string name="content_reported_as_spam_title">"Reported as spam"</string>
|
||||
<string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
|
||||
<string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string>
|
||||
<string name="content_reported_as_inappropriate_title">"Reported as inappropriate"</string>
|
||||
<string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
|
||||
<string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string>
|
||||
|
||||
<string name="permissions_rationale_msg_keys_backup_export">Element needs permission to save your E2E keys on disk.\n\nPlease allow access on the next pop-up to be able to export your keys manually.</string>
|
||||
|
||||
|
|
Loading…
Reference in New Issue