Merge branch 'develop' into feature/bma/open_settings
This commit is contained in:
commit
2125047ca5
|
@ -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 🗣:
|
||||||
-
|
-
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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>>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue