Merge branch 'develop' into feature/bma/open_settings

This commit is contained in:
Benoit Marty 2020-10-06 12:20:35 +02:00 committed by GitHub
commit 2125047ca5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 393 additions and 286 deletions

View File

@ -14,10 +14,12 @@ Improvements 🙌:
- Add a menu item to open the setting in room list and in room (#2171) - Add a menu item to open the setting in room list and in room (#2171)
- Add a menu item in the timeline as a shortcut to invite user (#2171) - Add a menu item in the timeline as a shortcut to invite user (#2171)
- Drawer: move settings access and add sign out action (#2171) - Drawer: move settings access and add sign out action (#2171)
- Filter room member (and banned users) by name (#2184)
Bugfix 🐛: Bugfix 🐛:
- Improve support for image/audio/video/file selection with intent changes (#1376) - Improve support for image/audio/video/file selection with intent changes (#1376)
- Fix Splash layout on small screens - Fix Splash layout on small screens
- Simplifies draft management and should fix bunch of draft issues (#952, #683)
Translations 🗣: Translations 🗣:
- -

View File

@ -101,8 +101,11 @@ class RxRoom(private val room: Room) {
return room.getEventReadReceiptsLive(eventId).asObservable() return room.getEventReadReceiptsLive(eventId).asObservable()
} }
fun liveDrafts(): Observable<List<UserDraft>> { fun liveDraft(): Observable<Optional<UserDraft>> {
return room.getDraftsLive().asObservable() return room.getDraftLive().asObservable()
.startWithCallable {
room.getDraft().toOptional()
}
} }
fun liveNotificationState(): Observable<RoomNotificationState> { fun liveNotificationState(): Observable<RoomNotificationState> {

View File

@ -20,6 +20,7 @@ package org.matrix.android.sdk.api.session.room.send
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
interface DraftService { interface DraftService {
@ -34,8 +35,12 @@ interface DraftService {
fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable
/** /**
* Return the current drafts if any, as a live data * Return the current draft or null
* The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts
*/ */
fun getDraftsLive(): LiveData<List<UserDraft>> fun getDraft(): UserDraft?
/**
* Return the current draft if any, as a live data
*/
fun getDraftLive(): LiveData<Optional<UserDraft>>
} }

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.room.send.DraftService import org.matrix.android.sdk.api.session.room.send.DraftService
import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
@ -55,7 +56,11 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private
} }
} }
override fun getDraftsLive(): LiveData<List<UserDraft>> { override fun getDraft(): UserDraft? {
return draftRepository.getDraft(roomId)
}
override fun getDraftLive(): LiveData<Optional<UserDraft>> {
return draftRepository.getDraftsLive(roomId) return draftRepository.getDraftsLive(roomId)
} }
} }

View File

@ -20,43 +20,67 @@ package org.matrix.android.sdk.internal.session.room.draft
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.kotlin.createObject
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.DraftMapper import org.matrix.android.sdk.internal.database.mapper.DraftMapper
import org.matrix.android.sdk.internal.database.model.DraftEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.UserDraftsEntity import org.matrix.android.sdk.internal.database.model.UserDraftsEntity
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import io.realm.Realm
import io.realm.kotlin.createObject
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { internal class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider) {
suspend fun saveDraft(roomId: String, userDraft: UserDraft) { suspend fun saveDraft(roomId: String, userDraft: UserDraft) {
monarchy.awaitTransaction { monarchy.awaitTransaction {
saveDraft(it, userDraft, roomId) saveDraftInDb(it, userDraft, roomId)
} }
} }
suspend fun deleteDraft(roomId: String) { suspend fun deleteDraft(roomId: String) {
monarchy.awaitTransaction { monarchy.awaitTransaction {
deleteDraft(it, roomId) deleteDraftFromDb(it, roomId)
} }
} }
private fun deleteDraft(realm: Realm, roomId: String) { fun getDraft(roomId: String): UserDraft? {
UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> return realmSessionProvider.withRealm { realm ->
if (userDraftsEntity.userDrafts.isNotEmpty()) { UserDraftsEntity.where(realm, roomId).findFirst()
userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) ?.userDrafts
?.firstOrNull()
?.let {
DraftMapper.map(it)
} }
} }
} }
private fun saveDraft(realm: Realm, draft: UserDraft, roomId: String) { fun getDraftsLive(roomId: String): LiveData<Optional<UserDraft>> {
val liveData = monarchy.findAllMappedWithChanges(
{ UserDraftsEntity.where(it, roomId) },
{
it.userDrafts.map { draft ->
DraftMapper.map(draft)
}
}
)
return Transformations.map(liveData) {
it.firstOrNull()?.firstOrNull().toOptional()
}
}
private fun deleteDraftFromDb(realm: Realm, roomId: String) {
UserDraftsEntity.where(realm, roomId).findFirst()?.userDrafts?.clear()
}
private fun saveDraftInDb(realm: Realm, draft: UserDraft, roomId: String) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)
@ -68,62 +92,15 @@ class DraftRepository @Inject constructor(@SessionDatabase private val monarchy:
userDraftsEntity.let { userDraftEntity -> userDraftsEntity.let { userDraftEntity ->
// Save only valid draft // Save only valid draft
if (draft.isValid()) { if (draft.isValid()) {
// Add a new draft or update the current one? // Replace the current draft
val newDraft = DraftMapper.map(draft) val newDraft = DraftMapper.map(draft)
// Is it an update of the top draft?
val topDraft = userDraftEntity.userDrafts.lastOrNull()
if (topDraft == null) {
Timber.d("Draft: create a new draft ${privacySafe(draft)}") Timber.d("Draft: create a new draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.clear()
userDraftEntity.userDrafts.add(newDraft) userDraftEntity.userDrafts.add(newDraft)
} else if (topDraft.draftMode == DraftEntity.MODE_EDIT) {
// top draft is an edit
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
if (topDraft.linkedEventId == newDraft.linkedEventId) {
// Update the top draft
Timber.d("Draft: update the top edit draft ${privacySafe(draft)}")
topDraft.content = newDraft.content
} else {
// Check a previously EDIT draft with the same id
val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find {
it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId
}
if (existingEditDraftOfSameEvent != null) {
// Ignore the new text, restore what was typed before, by putting the draft to the top
Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent)
userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent)
} else {
Timber.d("Draft: add a new edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
}
}
} else {
// Add a new regular draft to the top
Timber.d("Draft: add a new draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
}
} else {
// Top draft is not an edit
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
Timber.d("Draft: create a new edit draft ${privacySafe(draft)}")
userDraftEntity.userDrafts.add(newDraft)
} else {
// Update the top draft
Timber.d("Draft: update the top draft ${privacySafe(draft)}")
topDraft.draftMode = newDraft.draftMode
topDraft.content = newDraft.content
topDraft.linkedEventId = newDraft.linkedEventId
}
}
} else { } else {
// There is no draft to save, so the composer was clear // There is no draft to save, so the composer was clear
Timber.d("Draft: delete a draft") Timber.d("Draft: delete a draft")
val topDraft = userDraftEntity.userDrafts.lastOrNull() val topDraft = userDraftEntity.userDrafts.lastOrNull()
if (topDraft == null) { if (topDraft == null) {
Timber.d("Draft: nothing to do") Timber.d("Draft: nothing to do")
} else { } else {
@ -135,20 +112,6 @@ class DraftRepository @Inject constructor(@SessionDatabase private val monarchy:
} }
} }
fun getDraftsLive(roomId: String): LiveData<List<UserDraft>> {
val liveData = monarchy.findAllMappedWithChanges(
{ UserDraftsEntity.where(it, roomId) },
{
it.userDrafts.map { draft ->
DraftMapper.map(draft)
}
}
)
return Transformations.map(liveData) {
it.firstOrNull().orEmpty()
}
}
private fun privacySafe(o: Any): Any { private fun privacySafe(o: Any): Any {
if (BuildConfig.LOG_PRIVATE_DATA) { if (BuildConfig.LOG_PRIVATE_DATA) {
return o return o

View File

@ -51,7 +51,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction() data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction()
data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction() data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction()
data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction() data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction()
data class ExitSpecialMode(val text: String) : RoomDetailAction() data class EnterRegularMode(val text: String, val fromSharing: Boolean) : RoomDetailAction()
data class ResendMessage(val eventId: String) : RoomDetailAction() data class ResendMessage(val eventId: String) : RoomDetailAction()
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()

View File

@ -485,8 +485,7 @@ class RoomDetailFragment @Inject constructor(
if (savedInstanceState == null) { if (savedInstanceState == null) {
when (val sharedData = roomDetailArgs.sharedData) { when (val sharedData = roomDetailArgs.sharedData) {
is SharedData.Text -> { is SharedData.Text -> {
// Save a draft to set the shared text to the composer roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(sharedData.text, fromSharing = true))
roomDetailViewModel.handle(RoomDetailAction.SaveDraft(sharedData.text))
} }
is SharedData.Attachments -> { is SharedData.Attachments -> {
// open share edition // open share edition
@ -1022,7 +1021,7 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onCloseRelatedMessage() { override fun onCloseRelatedMessage() {
roomDetailViewModel.handle(RoomDetailAction.ExitSpecialMode(composerLayout.text.toString())) roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(composerLayout.text.toString(), false))
} }
override fun onRichContentSelected(contentUri: Uri): Boolean { override fun onRichContentSelected(contentUri: Uri): Boolean {
@ -1155,12 +1154,8 @@ class RoomDetailFragment @Inject constructor(
private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) { private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) {
when (sendMessageResult) { when (sendMessageResult) {
is RoomDetailViewEvents.MessageSent -> {
updateComposerText("")
}
is RoomDetailViewEvents.SlashCommandHandled -> { is RoomDetailViewEvents.SlashCommandHandled -> {
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
updateComposerText("")
} }
is RoomDetailViewEvents.SlashCommandError -> { is RoomDetailViewEvents.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))

View File

@ -164,7 +164,7 @@ class RoomDetailViewModel @AssistedInject constructor(
getUnreadState() getUnreadState()
observeSyncState() observeSyncState()
observeEventDisplayedActions() observeEventDisplayedActions()
observeDrafts() getDraftIfAny()
observeUnreadState() observeUnreadState()
observeMyRoomMember() observeMyRoomMember()
observeActiveRoomWidgets() observeActiveRoomWidgets()
@ -242,7 +242,7 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.RedactAction -> handleRedactEvent(action) is RoomDetailAction.RedactAction -> handleRedactEvent(action)
is RoomDetailAction.UndoReaction -> handleUndoReact(action) is RoomDetailAction.UndoReaction -> handleUndoReact(action)
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action) is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
is RoomDetailAction.EnterEditMode -> handleEditAction(action) is RoomDetailAction.EnterEditMode -> handleEditAction(action)
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
@ -451,48 +451,53 @@ class RoomDetailViewModel @AssistedInject constructor(
/** /**
* Convert a send mode to a draft and save the draft * Convert a send mode to a draft and save the draft
*/ */
private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) { private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState {
withState { when {
when (it.sendMode) { it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> {
is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback()) setState { copy(sendMode = it.sendMode.copy(action.draft)) }
is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback())
is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) }
is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) it.sendMode is SendMode.REPLY -> {
}.exhaustive setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
}
it.sendMode is SendMode.QUOTE -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
}
it.sendMode is SendMode.EDIT -> {
setState { copy(sendMode = it.sendMode.copy(text = action.draft)) }
room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback())
}
} }
} }
private fun observeDrafts() { private fun getDraftIfAny() {
room.rx().liveDrafts() val currentDraft = room.getDraft() ?: return
.subscribe {
Timber.d("Draft update --> SetState")
setState { setState {
val draft = it.lastOrNull() ?: UserDraft.REGULAR("")
copy( copy(
// Create a sendMode from a draft and retrieve the TimelineEvent // Create a sendMode from a draft and retrieve the TimelineEvent
sendMode = when (draft) { sendMode = when (currentDraft) {
is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) is UserDraft.REGULAR -> SendMode.REGULAR(currentDraft.text, false)
is UserDraft.QUOTE -> { is UserDraft.QUOTE -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
SendMode.QUOTE(timelineEvent, draft.text) SendMode.QUOTE(timelineEvent, currentDraft.text)
} }
} }
is UserDraft.REPLY -> { is UserDraft.REPLY -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
SendMode.REPLY(timelineEvent, draft.text) SendMode.REPLY(timelineEvent, currentDraft.text)
} }
} }
is UserDraft.EDIT -> { is UserDraft.EDIT -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent ->
SendMode.EDIT(timelineEvent, draft.text) SendMode.EDIT(timelineEvent, currentDraft.text)
} }
} }
} ?: SendMode.REGULAR("") } ?: SendMode.REGULAR("", fromSharing = false)
) )
} }
} }
.disposeOnClear()
}
private fun handleUserIsTyping(action: RoomDetailAction.UserIsTyping) { private fun handleUserIsTyping(action: RoomDetailAction.UserIsTyping) {
if (vectorPreferences.sendTypingNotifs()) { if (vectorPreferences.sendTypingNotifs()) {
@ -744,9 +749,16 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun popDraft() { private fun popDraft() = withState {
if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) {
// If we were sharing, we want to get back our last value from draft
getDraftIfAny()
} else {
// Otherwise we clear the composer and remove the draft from db
setState { copy(sendMode = SendMode.REGULAR("", false)) }
room.deleteDraft(NoOpMatrixCallback()) room.deleteDraft(NoOpMatrixCallback())
} }
}
private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) {
session.joinRoom(command.roomAlias, command.reason, emptyList(), object : MatrixCallback<Unit> { session.joinRoom(command.roomAlias, command.reason, emptyList(), object : MatrixCallback<Unit> {
@ -919,74 +931,25 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
private fun handleEditAction(action: RoomDetailAction.EnterEditMode) { private fun handleEditAction(action: RoomDetailAction.EnterEditMode) {
saveCurrentDraft(action.text)
room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.EDIT(timelineEvent, action.text)) } setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) }
timelineEvent.root.eventId?.let {
room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: ""), NoOpMatrixCallback())
}
} }
} }
private fun handleQuoteAction(action: RoomDetailAction.EnterQuoteMode) { private fun handleQuoteAction(action: RoomDetailAction.EnterQuoteMode) {
saveCurrentDraft(action.text)
room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) } setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) }
withState { state ->
// Save a new draft and keep the previously entered text, if it was not an edit
timelineEvent.root.eventId?.let {
if (state.sendMode is SendMode.EDIT) {
room.saveDraft(UserDraft.QUOTE(it, ""), NoOpMatrixCallback())
} else {
room.saveDraft(UserDraft.QUOTE(it, action.text), NoOpMatrixCallback())
}
}
}
} }
} }
private fun handleReplyAction(action: RoomDetailAction.EnterReplyMode) { private fun handleReplyAction(action: RoomDetailAction.EnterReplyMode) {
saveCurrentDraft(action.text)
room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) } setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) }
withState { state ->
// Save a new draft and keep the previously entered text, if it was not an edit
timelineEvent.root.eventId?.let {
if (state.sendMode is SendMode.EDIT) {
room.saveDraft(UserDraft.REPLY(it, ""), NoOpMatrixCallback())
} else {
room.saveDraft(UserDraft.REPLY(it, action.text), NoOpMatrixCallback())
}
}
}
}
}
private fun saveCurrentDraft(draft: String) {
// Save the draft with the current text if any
withState {
if (draft.isNotBlank()) {
when (it.sendMode) {
is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft), NoOpMatrixCallback())
is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback())
is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback())
is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback())
}
}
} }
} }
private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) = withState { private fun handleEnterRegularMode(action: RoomDetailAction.EnterRegularMode) = setState {
if (it.sendMode is SendMode.EDIT) { copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing))
room.deleteDraft(NoOpMatrixCallback())
} else {
// Save a new draft and keep the previously entered text
room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback())
}
setState { copy(sendMode = SendMode.REGULAR(action.text)) }
} }
private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) {

View File

@ -37,7 +37,12 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget
* Depending on the state the bottom toolbar will change (icons/preview/actions...) * Depending on the state the bottom toolbar will change (icons/preview/actions...)
*/ */
sealed class SendMode(open val text: String) { sealed class SendMode(open val text: String) {
data class REGULAR(override val text: String) : SendMode(text) data class REGULAR(
override val text: String,
val fromSharing: Boolean,
// This is necessary for forcing refresh on selectSubscribe
private val ts: Long = System.currentTimeMillis()
) : SendMode(text)
data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
@ -58,7 +63,7 @@ data class RoomDetailViewState(
val asyncRoomSummary: Async<RoomSummary> = Uninitialized, val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val activeRoomWidgets: Async<List<Widget>> = Uninitialized, val activeRoomWidgets: Async<List<Widget>> = Uninitialized,
val typingMessage: String? = null, val typingMessage: String? = null,
val sendMode: SendMode = SendMode.REGULAR(""), val sendMode: SendMode = SendMode.REGULAR("", false),
val tombstoneEvent: Event? = null, val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized, val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.Idle, val syncState: SyncState = SyncState.Idle,

View File

@ -25,12 +25,15 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_SWIPE import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_SWIPE
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyTouchHelperCallback import com.airbnb.epoxy.EpoxyTouchHelperCallback
import com.airbnb.epoxy.EpoxyViewHolder import com.airbnb.epoxy.EpoxyViewHolder
import im.vector.app.R
import im.vector.app.features.themes.ThemeUtils
import timber.log.Timber import timber.log.Timber
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
@ -52,7 +55,16 @@ class RoomMessageTouchHelperCallback(private val context: Context,
private var replyButtonProgress: Float = 0F private var replyButtonProgress: Float = 0F
private var lastReplyButtonAnimationTime: Long = 0 private var lastReplyButtonAnimationTime: Long = 0
private var imageDrawable: Drawable = ContextCompat.getDrawable(context, actionIcon)!! private val imageDrawable: Drawable = DrawableCompat.wrap(
ContextCompat.getDrawable(context, actionIcon)!!
)
init {
DrawableCompat.setTint(
imageDrawable,
ThemeUtils.getColor(context, R.attr.riotx_text_primary)
)
}
private val triggerDistance = convertToPx(100) private val triggerDistance = convertToPx(100)
private val minShowDistance = convertToPx(20) private val minShowDistance = convertToPx(20)

View File

@ -25,7 +25,7 @@ class RoomListNameFilter @Inject constructor() : Predicate<RoomSummary> {
var filter: String = "" var filter: String = ""
override fun test(roomSummary: RoomSummary): Boolean { override fun test(roomSummary: RoomSummary): Boolean {
if (filter.isBlank()) { if (filter.isEmpty()) {
// No filter // No filter
return true return true
} }

View File

@ -22,4 +22,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class RoomBannedListMemberAction : VectorViewModelAction { sealed class RoomBannedListMemberAction : VectorViewModelAction {
data class QueryInfo(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction() data class QueryInfo(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction()
data class UnBanUser(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.rx
import org.matrix.android.sdk.rx.unwrap import org.matrix.android.sdk.rx.unwrap
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
@ -90,6 +91,15 @@ class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initia
when (action) { when (action) {
is RoomBannedListMemberAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary) is RoomBannedListMemberAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary)
is RoomBannedListMemberAction.UnBanUser -> unBanUser(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 package im.vector.app.features.roomprofile.banned
import com.airbnb.epoxy.TypedEpoxyController 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.R
import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.epoxy.profiles.buildProfileSection 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.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.home.AvatarRenderer 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 import javax.inject.Inject
class RoomBannedMemberListController @Inject constructor( class RoomBannedMemberListController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val roomMemberSummaryFilter: RoomMemberSummaryFilter,
colorProvider: ColorProvider colorProvider: ColorProvider
) : TypedEpoxyController<RoomBannedMemberListViewState>() { ) : TypedEpoxyController<RoomBannedMemberListViewState>() {
@ -63,7 +65,10 @@ class RoomBannedMemberListController @Inject constructor(
} else { } else {
buildProfileSection(quantityString) buildProfileSection(quantityString)
bannedList.join( roomMemberSummaryFilter.filter = data.filter
bannedList
.filter { roomMemberSummaryFilter.test(it) }
.join(
each = { _, roomMember -> each = { _, roomMember ->
val actionInProgress = data.onGoingModerationAction.contains(roomMember.userId) val actionInProgress = data.onGoingModerationAction.contains(roomMember.userId)
profileMatrixItemWithProgress { profileMatrixItemWithProgress {

View File

@ -19,6 +19,8 @@ package im.vector.app.features.roomprofile.banned
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -53,6 +55,7 @@ class RoomBannedMemberListFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
roomMemberListController.callback = this roomMemberListController.callback = this
setupToolbar(roomSettingsToolbar) setupToolbar(roomSettingsToolbar)
setupSearchView()
recyclerView.configureWith(roomMemberListController, hasFixedSize = true) recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
viewModel.observeViewEvents { viewModel.observeViewEvents {
@ -84,6 +87,21 @@ class RoomBannedMemberListFragment @Inject constructor(
super.onDestroyView() 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 -> override fun invalidate() = withState(viewModel) { viewState ->
roomMemberListController.setData(viewState) roomMemberListController.setData(viewState)
renderRoomSummary(viewState) renderRoomSummary(viewState)

View File

@ -27,6 +27,7 @@ data class RoomBannedMemberListViewState(
val roomId: String, val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized, val roomSummary: Async<RoomSummary> = Uninitialized,
val bannedMemberSummaries: Async<List<RoomMemberSummary>> = Uninitialized, val bannedMemberSummaries: Async<List<RoomMemberSummary>> = Uninitialized,
val filter: String = "",
val onGoingModerationAction: List<String> = emptyList(), val onGoingModerationAction: List<String> = emptyList(),
val canUserBan: Boolean = false val canUserBan: Boolean = false
) : MvRxState { ) : MvRxState {

View File

@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class RoomMemberListAction : VectorViewModelAction { sealed class RoomMemberListAction : VectorViewModelAction {
data class RevokeThreePidInvite(val stateKey: String) : RoomMemberListAction() 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 package im.vector.app.features.roomprofile.members
import com.airbnb.epoxy.TypedEpoxyController 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.R
import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.epoxy.profiles.buildProfileSection 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.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer 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 import javax.inject.Inject
class RoomMemberListController @Inject constructor( class RoomMemberListController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val roomMemberSummaryFilter: RoomMemberSummaryFilter,
colorProvider: ColorProvider colorProvider: ColorProvider
) : TypedEpoxyController<RoomMemberListViewState>() { ) : TypedEpoxyController<RoomMemberListViewState>() {
interface Callback { interface Callback {
fun onRoomMemberClicked(roomMember: RoomMemberSummary) fun onRoomMemberClicked(roomMember: RoomMemberSummary)
fun onThreePidInvites(event: Event) fun onThreePidInviteClicked(event: Event)
} }
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
@ -53,17 +54,29 @@ class RoomMemberListController @Inject constructor(
} }
override fun buildModels(data: RoomMemberListViewState?) { override fun buildModels(data: RoomMemberListViewState?) {
val roomMembersByPowerLevel = data?.roomMemberSummaries?.invoke() ?: return data ?: return
val threePidInvites = data.threePidInvites().orEmpty()
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() var threePidInvitesDone = threePidInvites.isEmpty()
for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) { for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) {
if (roomMemberList.isEmpty()) { val filteredRoomMemberList = roomMemberList.filter { roomMemberSummaryFilter.test(it) }
if (filteredRoomMemberList.isEmpty()) {
continue continue
} }
if (powerLevelCategory == RoomMemberListCategories.USER && !threePidInvitesDone) { 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( buildProfileSection(
stringProvider.getString(RoomMemberListCategories.INVITE.titleRes) stringProvider.getString(RoomMemberListCategories.INVITE.titleRes)
) )
@ -75,7 +88,7 @@ class RoomMemberListController @Inject constructor(
buildProfileSection( buildProfileSection(
stringProvider.getString(powerLevelCategory.titleRes) stringProvider.getString(powerLevelCategory.titleRes)
) )
roomMemberList.join( filteredRoomMemberList.join(
each = { _, roomMember -> each = { _, roomMember ->
profileMatrixItem { profileMatrixItem {
id(roomMember.userId) 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 // Display the threepid invite after the regular invite
dividerItem { dividerItem {
id("divider_threepidinvites") id("divider_threepidinvites")
color(dividerColor) color(dividerColor)
} }
buildThreePidInvites(data) buildThreePidInvites(data)
threePidInvitesDone = true threePidInvitesDone = true
} }
@ -128,7 +142,7 @@ class RoomMemberListController @Inject constructor(
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
editable(data.actionsPermissions.canRevokeThreePidInvite) editable(data.actionsPermissions.canRevokeThreePidInvite)
clickListener { _ -> clickListener { _ ->
callback?.onThreePidInvites(event) callback?.onThreePidInviteClicked(event)
} }
} }
} }

View File

@ -21,6 +21,8 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -72,12 +74,28 @@ class RoomMemberListFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
roomMemberListController.callback = this roomMemberListController.callback = this
setupToolbar(roomSettingsToolbar) setupToolbar(roomSettingsToolbar)
setupSearchView()
recyclerView.configureWith(roomMemberListController, hasFixedSize = true) recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
viewModel.selectSubscribe(this, RoomMemberListViewState::actionsPermissions) { viewModel.selectSubscribe(this, RoomMemberListViewState::actionsPermissions) {
invalidateOptionsMenu() 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() { override fun onDestroyView() {
recyclerView.cleanup() recyclerView.cleanup()
super.onDestroyView() super.onDestroyView()
@ -92,7 +110,7 @@ class RoomMemberListFragment @Inject constructor(
navigator.openRoomMemberProfile(roomMember.userId, roomId = roomProfileArgs.roomId, context = requireActivity()) 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 // Display a dialog to revoke invite if power level is high enough
val content = event.content.toModel<RoomThirdPartyInviteContent>() ?: return val content = event.content.toModel<RoomThirdPartyInviteContent>() ?: return
val stateKey = event.stateKey ?: return val stateKey = event.stateKey ?: return

View File

@ -188,6 +188,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
override fun handle(action: RoomMemberListAction) { override fun handle(action: RoomMemberListAction) {
when (action) { when (action) {
is RoomMemberListAction.RevokeThreePidInvite -> handleRevokeThreePidInvite(action) is RoomMemberListAction.RevokeThreePidInvite -> handleRevokeThreePidInvite(action)
is RoomMemberListAction.FilterMemberList -> handleFilterMemberList(action)
}.exhaustive }.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 roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized, val roomSummary: Async<RoomSummary> = Uninitialized,
val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized, val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized,
val filter: String = "",
val threePidInvites: Async<List<Event>> = Uninitialized, val threePidInvites: Async<List<Event>> = Uninitialized,
val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized, val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized,
val actionsPermissions: ActionPermissions = ActionPermissions() 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

@ -53,17 +53,54 @@
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>
<androidx.recyclerview.widget.RecyclerView <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/recyclerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:overScrollMode="always"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="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" /> 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" /> <include layout="@layout/merge_overlay_waiting_view" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -105,6 +105,7 @@
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:src="@drawable/ic_edit" android:src="@drawable/ic_edit"
android:visibility="gone" android:visibility="gone"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="@+id/roomNameView" app:layout_constraintBottom_toBottomOf="@+id/roomNameView"
app:layout_constraintEnd_toStartOf="@+id/roomUnreadCounterBadgeView" app:layout_constraintEnd_toStartOf="@+id/roomUnreadCounterBadgeView"
app:layout_constraintStart_toEndOf="@+id/roomNameView" app:layout_constraintStart_toEndOf="@+id/roomNameView"

View File

@ -623,6 +623,7 @@
<!-- Search --> <!-- Search -->
<string name="search_hint">Search</string> <string name="search_hint">Search</string>
<string name="search_members_hint">Filter room members</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="search_no_results">No results</string>
<string name="tab_title_search_rooms">ROOMS</string> <string name="tab_title_search_rooms">ROOMS</string>
<string name="tab_title_search_messages">MESSAGES</string> <string name="tab_title_search_messages">MESSAGES</string>
@ -1851,11 +1852,11 @@
<string name="block_user">"IGNORE USER"</string> <string name="block_user">"IGNORE USER"</string>
<string name="content_reported_title">"Content reported"</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_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_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> <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>