Merge branch 'develop' into feature/ons/fix_email_confirmation_flow

This commit is contained in:
Onuray Sahin 2020-10-06 11:16:50 +03:00 committed by GitHub
commit 1f4a360a0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1993 additions and 64 deletions

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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.

View File

@ -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
}

View File

@ -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?
)

View File

@ -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
}

View File

@ -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()
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
)
}
}

View File

@ -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)
}
}

View File

@ -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>
}

View File

@ -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
}

View File

@ -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()
)
}
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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

View File

@ -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 -->

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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) }
}
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -109,4 +109,6 @@ interface Navigator {
view: View,
inMemory: List<AttachmentData> = emptyList(),
options: ((MutableList<Pair<View, String>>) -> Unit)?)
fun openSearch(context: Context, roomId: String)
}

View File

@ -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()
}

View File

@ -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
)
}
}

View File

@ -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,34 +65,37 @@ class RoomBannedMemberListController @Inject constructor(
} else {
buildProfileSection(quantityString)
bannedList.join(
each = { _, roomMember ->
val actionInProgress = data.onGoingModerationAction.contains(roomMember.userId)
profileMatrixItemWithProgress {
id(roomMember.userId)
matrixItem(roomMember.toMatrixItem())
avatarRenderer(avatarRenderer)
apply {
if (actionInProgress) {
inProgress(true)
editable(false)
} else {
inProgress(false)
editable(true)
clickListener { _ ->
callback?.onUnbanClicked(roomMember)
roomMemberSummaryFilter.filter = data.filter
bannedList
.filter { roomMemberSummaryFilter.test(it) }
.join(
each = { _, roomMember ->
val actionInProgress = data.onGoingModerationAction.contains(roomMember.userId)
profileMatrixItemWithProgress {
id(roomMember.userId)
matrixItem(roomMember.toMatrixItem())
avatarRenderer(avatarRenderer)
apply {
if (actionInProgress) {
inProgress(true)
editable(false)
} else {
inProgress(false)
editable(true)
clickListener { _ ->
callback?.onUnbanClicked(roomMember)
}
}
}
}
},
between = { _, roomMemberBefore ->
dividerItem {
id("divider_${roomMemberBefore.userId}")
color(dividerColor)
}
}
}
},
between = { _, roomMemberBefore ->
dividerItem {
id("divider_${roomMemberBefore.userId}")
color(dividerColor)
}
}
)
)
}
}
}

View File

@ -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)

View File

@ -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 {

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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
)
}
}
}

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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()
@ -70,7 +68,7 @@ class RoomUploadsFragment @Inject constructor(
is RoomUploadsViewEvents.FileReadyForSharing -> {
shareMedia(requireContext(), it.file, getMimeTypeFromUri(requireContext(), it.file.toUri()))
}
is RoomUploadsViewEvents.FileReadyForSaving -> {
is RoomUploadsViewEvents.FileReadyForSaving -> {
saveMedia(
context = requireContext(),
file = it.file,
@ -79,7 +77,7 @@ class RoomUploadsFragment @Inject constructor(
notificationUtils = notificationUtils
)
}
is RoomUploadsViewEvents.Failure -> showFailure(it.throwable)
is RoomUploadsViewEvents.Failure -> showFailure(it.throwable)
}.exhaustive
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thicknessRatio="2"
android:useLevel="false">
<solid android:color="@android:color/transparent" />
<stroke android:width="2dp" />
</shape>
</item>
<item
android:bottom="14dp"
android:drawable="@drawable/ic_search"
android:left="14dp"
android:right="14dp"
android:top="14dp" />
</layer-list>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/searchToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
app:contentInsetStart="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.SearchView
android:id="@+id/searchView"
style="@style/VectorSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/base_color"
app:queryHint="@string/search_hint" />
</androidx.appcompat.widget.Toolbar>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/searchFragmentContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/searchToolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -53,16 +53,53 @@
</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"
tools:listitem="@layout/item_profile_action" />
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" />

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<im.vector.app.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/stateView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchResultRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="always"
tools:listitem="@layout/item_search_result" />
</im.vector.app.core.platform.StateView>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<ImageView
android:id="@+id/messageAvatarImageView"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/messageMemberNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
app:layout_constraintStart_toEndOf="@+id/messageAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/displayName" />
<TextView
android:id="@+id/messageTimeView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/messageMemberNameView"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/messageMemberNameView"
app:layout_constraintEnd_toEndOf="parent"
tools:text="@tools:sample/date/hhmm" />
<TextView
android:id="@+id/messageContentView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/messageMemberNameView"
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView"
tools:text="@sample/matrix.json/data/message" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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"

View File

@ -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>