diff --git a/CHANGES.md b/CHANGES.md index 42fb2cc291..1c0e8798de 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ Improvements: - Persist active tab between sessions (#503) - Do not upload file too big for the homeserver (#587) - Handle read markers (#84) + - Mark all messages as read (#396) + - Add ability to report content (#515) Other changes: - Accessibility improvements to read receipts in the room timeline and reactions emoji chooser @@ -19,6 +21,7 @@ Bugfix: - after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267) - Picture uploads are unreliable, pictures are shown in wrong aspect ratio on desktop client (#517) - Invitation notifications are not dismissed automatically if room is joined from another client (#347) + - Opening links from RiotX reuses browser tab (#599) Translations: - diff --git a/gradle.properties b/gradle.properties index 35ca815df8..2e2b110f15 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m vector.debugPrivateData=false -vector.httpLogLevel=HEADERS +vector.httpLogLevel=NONE # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above #vector.debugPrivateData=true diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 328cfbee20..3e6d3ea88b 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -102,7 +102,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.recyclerview:recyclerview:1.1.0-beta04" + implementation "androidx.recyclerview:recyclerview:1.1.0-beta05" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" @@ -110,8 +110,8 @@ dependencies { // Network implementation 'com.squareup.retrofit2:retrofit:2.6.2' implementation 'com.squareup.retrofit2:converter-moshi:2.6.2' - implementation 'com.squareup.okhttp3:okhttp:4.2.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0' + implementation 'com.squareup.okhttp3:okhttp:4.2.2' + implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' implementation 'com.novoda:merlin:1.2.0' implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 9f91e5b276..70c9c6e36c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.relation.RelationService +import im.vector.matrix.android.api.session.room.reporting.ReportingService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService @@ -38,6 +39,7 @@ interface Room : ReadService, MembershipService, StateService, + ReportingService, RelationService, RoomCryptoService { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index 175d393c86..c7fedb2627 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -53,4 +53,9 @@ interface RoomService { * @return the [LiveData] of [RoomSummary] */ fun liveRoomSummaries(): LiveData> + + /** + * Mark all rooms as read + */ + fun markAllAsRead(roomIds: List, callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index c6a58eeec1..5af5183dfa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -50,21 +50,19 @@ interface RelationService { /** * Sends a reaction (emoji) to the targetedEvent. - * @param reaction the reaction (preferably emoji) * @param targetEventId the id of the event being reacted + * @param reaction the reaction (preferably emoji) */ - fun sendReaction(reaction: String, - targetEventId: String): Cancelable + fun sendReaction(targetEventId: String, + reaction: String): Cancelable /** * Undo a reaction (emoji) to the targetedEvent. - * @param reaction the reaction (preferably emoji) * @param targetEventId the id of the event being reacted - * @param myUserId used to know if a reaction event was made by the user + * @param reaction the reaction (preferably emoji) */ - fun undoReaction(reaction: String, - targetEventId: String, - myUserId: String) // : Cancelable + fun undoReaction(targetEventId: String, + reaction: String): Cancelable /** * Edit a text message body. Limited to "m.text" contentType @@ -92,7 +90,7 @@ interface RelationService { compatibilityBodyText: String = "* $newBodyText"): Cancelable /** - * Get's the edit history of the given event + * Get the edit history of the given event */ fun fetchEditHistory(eventId: String, callback: MatrixCallback>) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt new file mode 100644 index 0000000000..71ce02ac69 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 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.matrix.android.api.session.room.reporting + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +/** + * This interface defines methods to report content of an event. + */ +interface ReportingService { + + /** + * Report content + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid + */ + fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index d3d96c16d1..bb629fd881 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -766,7 +766,6 @@ internal class DefaultCryptoService @Inject constructor( * * @param password the password * @param anIterationCount the encryption iteration count (0 means no encryption) - * @param callback the exported keys */ private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray { return withContext(coroutineDispatchers.crypto) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 45dbeb43c3..897a1f0a5d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -156,7 +156,6 @@ internal class MXMegolmEncryption( * * @param session the session info * @param devicesByUser the devices map - * @param callback the asynchronous callback */ private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo, devicesByUser: Map>) { @@ -262,7 +261,6 @@ internal class MXMegolmEncryption( * This method must be called in getDecryptingThreadHandler() thread. * * @param userIds the user ids whose devices must be checked. - * @param callback the asynchronous callback */ private suspend fun getDevicesInRoom(userIds: List): MXUsersDevicesMap { // We are happy to use a cached version here: we assume that if we already diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt index ab665dbf99..e640e807c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt @@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrap import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.task.Task @@ -81,6 +82,7 @@ import kotlin.random.Random @SessionScope internal class KeysBackup @Inject constructor( + @UserId private val userId: String, private val credentials: Credentials, private val cryptoStore: IMXCryptoStore, private val olmDevice: MXOlmDevice, @@ -379,8 +381,6 @@ internal class KeysBackup @Inject constructor( */ @WorkerThread private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust { - val myUserId = credentials.userId - val keysBackupVersionTrust = KeysBackupVersionTrust() val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData() @@ -392,7 +392,7 @@ internal class KeysBackup @Inject constructor( return keysBackupVersionTrust } - val mySigs = authData.signatures?.get(myUserId) + val mySigs = authData.signatures?.get(userId) if (mySigs.isNullOrEmpty()) { Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user") return keysBackupVersionTrust @@ -407,7 +407,7 @@ internal class KeysBackup @Inject constructor( } if (deviceId != null) { - val device = cryptoStore.getUserDevice(deviceId, myUserId) + val device = cryptoStore.getUserDevice(deviceId, userId) var isSignatureValid = false if (device == null) { @@ -454,10 +454,8 @@ internal class KeysBackup @Inject constructor( } else { GlobalScope.launch(coroutineDispatchers.main) { val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { - val myUserId = credentials.userId - // Get current signatures, or create an empty set - val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap() + val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap() ?: HashMap() if (trust) { @@ -466,7 +464,7 @@ internal class KeysBackup @Inject constructor( val deviceSignatures = objectSigner.signObject(canonicalJson) - deviceSignatures[myUserId]?.forEach { entry -> + deviceSignatures[userId]?.forEach { entry -> myUserSignatures[entry.key] = entry.value } } else { @@ -482,7 +480,7 @@ internal class KeysBackup @Inject constructor( val newMegolmBackupAuthData = authData.copy() val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap() - newSignatures[myUserId] = myUserSignatures + newSignatures[userId] = myUserSignatures newMegolmBackupAuthData.signatures = newSignatures @@ -1358,5 +1356,5 @@ internal class KeysBackup @Inject constructor( * DEBUG INFO * ========================================================================================== */ - override fun toString() = "KeysBackup for ${credentials.userId}" + override fun toString() = "KeysBackup for $userId" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 7d957ccdad..fea827fd25 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.relation.RelationService +import im.vector.matrix.android.api.session.room.reporting.ReportingService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService @@ -44,18 +45,20 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val sendService: SendService, private val draftService: DraftService, private val stateService: StateService, + private val reportingService: ReportingService, private val readService: ReadService, private val cryptoService: CryptoService, private val relationService: RelationService, - private val roomMembersService: MembershipService -) : Room, - TimelineService by timelineService, - SendService by sendService, - DraftService by draftService, - StateService by stateService, - ReadService by readService, - RelationService by relationService, - MembershipService by roomMembersService { + private val roomMembersService: MembershipService) : + Room, + TimelineService by timelineService, + SendService by sendService, + DraftService by draftService, + StateService by stateService, + ReportingService by reportingService, + ReadService by readService, + RelationService by relationService, + MembershipService by roomMembersService { override fun getRoomSummaryLive(): LiveData> { val liveData = monarchy.findAllMappedWithChanges( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index c64676c8c2..962b7b54d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask +import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import io.realm.Realm @@ -41,6 +42,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona private val roomSummaryMapper: RoomSummaryMapper, private val createRoomTask: CreateRoomTask, private val joinRoomTask: JoinRoomTask, + private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val roomFactory: RoomFactory, private val taskExecutor: TaskExecutor) : RoomService { @@ -80,4 +82,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona } .executeBy(taskExecutor) } + + override fun markAllAsRead(roomIds: List, callback: MatrixCallback): Cancelable { + return markAllRoomsReadTask + .configureWith(MarkAllRoomsReadTask.Params(roomIds)) { + this.callback = callback + } + .executeBy(taskExecutor) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index daee1c914c..797dbed31c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody import im.vector.matrix.android.internal.session.room.relation.RelationsResponse +import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse @@ -245,4 +246,16 @@ internal interface RoomAPI { @Path("eventId") parent_id: String, @Body reason: Map ): Call + + /** + * Reports an event as inappropriate to the server, which may then notify the appropriate people. + * + * @param roomId the room id + * @param eventId the event to report content + * @param body body containing score and reason + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}") + fun reportContent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Body body: ReportContentBody): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 65a3624d2c..e2199782f4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.read.DefaultReadService import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService +import im.vector.matrix.android.internal.session.room.reporting.DefaultReportingService import im.vector.matrix.android.internal.session.room.send.DefaultSendService import im.vector.matrix.android.internal.session.room.state.DefaultStateService import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService @@ -40,6 +41,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona private val sendServiceFactory: DefaultSendService.Factory, private val draftServiceFactory: DefaultDraftService.Factory, private val stateServiceFactory: DefaultStateService.Factory, + private val reportingServiceFactory: DefaultReportingService.Factory, private val readServiceFactory: DefaultReadService.Factory, private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory) : @@ -54,6 +56,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona sendServiceFactory.create(roomId), draftServiceFactory.create(roomId), stateServiceFactory.create(roomId), + reportingServiceFactory.create(roomId), readServiceFactory.create(roomId), cryptoService, relationServiceFactory.create(roomId), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index b3db84c9c6..1aca492b94 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -40,22 +40,16 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.Default import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask import im.vector.matrix.android.internal.session.room.prune.PruneEventTask +import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask +import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask -import im.vector.matrix.android.internal.session.room.relation.DefaultFetchEditHistoryTask -import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask -import im.vector.matrix.android.internal.session.room.relation.FetchEditHistoryTask -import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask +import im.vector.matrix.android.internal.session.room.relation.* +import im.vector.matrix.android.internal.session.room.reporting.DefaultReportContentTask +import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.timeline.* -import im.vector.matrix.android.internal.session.room.timeline.ClearUnlinkedEventsTask -import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask -import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import retrofit2.Retrofit @Module @@ -110,6 +104,9 @@ internal abstract class RoomModule { @Binds abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask + @Binds + abstract fun bindMarkAllRoomsReadTask(markAllRoomsReadTask: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask + @Binds abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask @@ -119,6 +116,9 @@ internal abstract class RoomModule { @Binds abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask + @Binds + abstract fun bindReportContentTask(reportContentTask: DefaultReportContentTask): ReportContentTask + @Binds abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt new file mode 100644 index 0000000000..99376a981a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/MarkAllRoomsReadTask.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.room.read + +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface MarkAllRoomsReadTask : Task { + data class Params( + val roomIds: List + ) +} + +internal class DefaultMarkAllRoomsReadTask @Inject constructor(private val readMarkersTask: SetReadMarkersTask) : MarkAllRoomsReadTask { + + override suspend fun execute(params: MarkAllRoomsReadTask.Params) { + params.roomIds.forEach { roomId -> + readMarkersTask.execute(SetReadMarkersTask.Params(roomId, markAllAsRead = true)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 6abc7ed60e..68669171c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -65,7 +65,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv fun create(roomId: String): RelationService } - override fun sendReaction(reaction: String, targetEventId: String): Cancelable { + override fun sendReaction(targetEventId: String, reaction: String): Cancelable { val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) .also { saveLocalEcho(it) @@ -75,13 +75,13 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv return CancelableWork(context, sendRelationWork.id) } - override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { + override fun undoReaction(targetEventId: String, reaction: String): Cancelable { val params = FindReactionEventForUndoTask.Params( roomId, targetEventId, - reaction, - myUserId + reaction ) + // TODO We should avoid using MatrixCallback internally val callback = object : MatrixCallback { override fun onSuccess(data: FindReactionEventForUndoTask.Result) { if (data.redactEventId == null) { @@ -89,7 +89,6 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv // TODO? } data.redactEventId?.let { toRedact -> - val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also { saveLocalEcho(it) } @@ -99,7 +98,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv } } } - findReactionEventForUndoTask + return findReactionEventForUndoTask .configureWith(params) { this.retryCount = Int.MAX_VALUE this.callback = callback diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt index 5f9f9e83ea..baa01e4042 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.Task import io.realm.Realm import javax.inject.Inject @@ -29,8 +30,7 @@ internal interface FindReactionEventForUndoTask : Task - getReactionToRedact(realm, params.reaction, params.eventId, params.myUserId)?.eventId + getReactionToRedact(realm, params.reaction, params.eventId)?.eventId } return FindReactionEventForUndoTask.Result(eventId) } - private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? { + private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String): EventEntity? { val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return null val rase = summary.reactionsSummary.where() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt index e4b71c0d42..6ec316e9a4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.Task import io.realm.Realm import javax.inject.Inject @@ -30,8 +31,7 @@ internal interface UpdateQuickReactionTask : Task?>? = null monarchy.doWithRealm { realm -> - res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId, params.myUserId) + res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId) } return UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList()) } - private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String, myUserId: String): Pair?> { + private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String): Pair?> { // the emoji reaction has been selected, we need to check if we have reacted it or not val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return Pair(reaction, null) @@ -68,7 +69,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull { // find source event val entity = EventEntity.where(realm, it).findFirst() - if (entity?.sender == myUserId) entity.eventId else null + if (entity?.sender == userId) entity.eventId else null } return Pair(reaction, toRedact) } else { @@ -77,7 +78,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo val toRedact = aggregationForReaction.sourceEvents.mapNotNull { // find source event val entity = EventEntity.where(realm, it).findFirst() - if (entity?.sender == myUserId) entity.eventId else null + if (entity?.sender == userId) entity.eventId else null } return Pair(null, toRedact) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/DefaultReportingService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/DefaultReportingService.kt new file mode 100644 index 0000000000..3b64cce439 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/DefaultReportingService.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.room.reporting + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.room.reporting.ReportingService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith + +internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val reportContentTask: ReportContentTask +) : ReportingService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): ReportingService + } + + override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable { + val params = ReportContentTask.Params(roomId, eventId, score, reason) + + return reportContentTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentBody.kt new file mode 100644 index 0000000000..2cf33551ce --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentBody.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.room.reporting + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ReportContentBody( + /** + * Required. The score to rate this content as where -100 is most offensive and 0 is inoffensive. + */ + @Json(name = "score") val score: Int, + + /** + * Required. The reason the content is being reported. May be blank. + */ + @Json(name = "reason") val reason: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt new file mode 100644 index 0000000000..60c031158a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 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.matrix.android.internal.session.room.reporting + +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface ReportContentTask : Task { + data class Params( + val roomId: String, + val eventId: String, + val score: Int, + val reason: String + ) +} + +internal class DefaultReportContentTask @Inject constructor(private val roomAPI: RoomAPI) : ReportContentTask { + override suspend fun execute(params: ReportContentTask.Params) { + return executeRequest { + apiCall = roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason)) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt index 4dbcc7168f..62fbd42ed5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/ReadReceiptHandler.kt @@ -25,7 +25,7 @@ import io.realm.Realm import timber.log.Timber import javax.inject.Inject -// the receipts dictionnaries +// the receipts dictionaries // key : $EventId // value : dict key $UserId // value dict key ts diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt index 36aded79ad..42e7e850b3 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.Optional import org.junit.Assert import org.junit.Test @@ -172,7 +173,6 @@ class PushrulesConditionTest { } class MockRoomService() : RoomService { - override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } @@ -192,9 +192,21 @@ class PushrulesConditionTest { override fun liveRoomSummaries(): LiveData> { return MutableLiveData() } + + override fun markAllAsRead(roomIds: List, callback: MatrixCallback): Cancelable { + TODO("not implemented") // To change body of created functions use File | Settings | File Templates. + } } class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room { + override fun getReadMarkerLive(): LiveData> { + TODO("not implemented") // To change body of created functions use File | Settings | File Templates. + } + + override fun getMyReadReceiptLive(): LiveData> { + TODO("not implemented") // To change body of created functions use File | Settings | File Templates. + } + override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } @@ -242,7 +254,7 @@ class PushrulesConditionTest { override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) { } - override fun liveTimeLineEvent(eventId: String): LiveData { + override fun getTimeLineEventLive(eventId: String): LiveData> { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } @@ -250,7 +262,7 @@ class PushrulesConditionTest { return _numberOfJoinedMembers } - override fun liveRoomSummary(): LiveData { + override fun getRoomSummaryLive(): LiveData> { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } @@ -330,11 +342,11 @@ class PushrulesConditionTest { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } - override fun sendReaction(reaction: String, targetEventId: String): Cancelable { + override fun sendReaction(targetEventId: String, reaction: String): Cancelable { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } - override fun undoReaction(reaction: String, targetEventId: String, myUserId: String) { + override fun undoReaction(targetEventId: String, reaction: String): Cancelable { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } @@ -347,7 +359,7 @@ class PushrulesConditionTest { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } - override fun getEventSummaryLive(eventId: String): LiveData { + override fun getEventSummaryLive(eventId: String): LiveData> { TODO("not implemented") // To change body of created functions use File | Settings | File Templates. } diff --git a/vector/build.gradle b/vector/build.gradle index 3992bba8a1..78018a6107 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -281,7 +281,7 @@ dependencies { // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.1.0-alpha10' + implementation 'com.google.android.material:material:1.1.0-beta01' implementation 'me.gujun.android:span:1.7' implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version" diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMaterialThemeActivity.kt b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMaterialThemeActivity.kt index ab6b86801a..542b0a1cbb 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMaterialThemeActivity.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMaterialThemeActivity.kt @@ -59,7 +59,7 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() { } override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.vector_home, menu) + menuInflater.inflate(R.menu.home, menu) return true } } diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 3b18d3042e..2cbc3d2a8b 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -42,6 +42,8 @@ import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.invite.VectorInviteView @@ -103,12 +105,10 @@ interface ScreenComponent { fun inject(messageActionsBottomSheet: MessageActionsBottomSheet) - fun inject(viewReactionBottomSheet: ViewReactionBottomSheet) + fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet) fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet) - fun inject(messageMenuFragment: MessageMenuFragment) - fun inject(vectorSettingsActivity: VectorSettingsActivity) fun inject(createRoomFragment: CreateRoomFragment) @@ -135,8 +135,6 @@ interface ScreenComponent { fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment) - fun inject(quickReactionFragment: QuickReactionFragment) - fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity) fun inject(loginActivity: LoginActivity) diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/Extensions.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/Extensions.kt new file mode 100644 index 0000000000..1b90e88864 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/Extensions.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 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.riotx.core.dialogs + +import androidx.annotation.ColorRes +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import im.vector.riotx.R + +fun AlertDialog.withColoredButton(whichButton: Int, @ColorRes color: Int = R.color.vector_error_color): AlertDialog { + getButton(whichButton)?.setTextColor(ContextCompat.getColor(context, color)) + return this +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt similarity index 71% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/VectorBaseBottomSheetDialogFragment.kt rename to vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt index 026cb3ba1c..8d40d55a7a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -13,18 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.core.platform +import android.app.Dialog import android.content.Context import android.os.Bundle import android.os.Parcelable +import android.widget.FrameLayout +import androidx.annotation.CallSuper import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRxView import com.airbnb.mvrx.MvRxViewModelStore +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.utils.DimensionConverter import java.util.* /** @@ -37,10 +42,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() private lateinit var screenComponent: ScreenComponent final override val mvrxViewId: String by lazy { mvrxPersistedViewId } + private var bottomSheetBehavior: BottomSheetBehavior? = null + val vectorBaseActivity: VectorBaseActivity by lazy { activity as VectorBaseActivity } + open val showExpanded = false + override fun onAttach(context: Context) { screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) super.onAttach(context) @@ -57,6 +66,17 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() super.onCreate(savedInstanceState) } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + val dialog = this as? BottomSheetDialog + bottomSheetBehavior = dialog?.behavior + bottomSheetBehavior?.setPeekHeight(DimensionConverter(resources).dpToPx(400), false) + if (showExpanded) { + bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + } + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) mvrxViewModelStore.saveViewModels(outState) @@ -70,6 +90,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() postInvalidate() } + @CallSuper + override fun invalidate() { + if (showExpanded) { + // Force the bottom sheet to be expanded + bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + } + protected fun setArguments(args: Parcelable? = null) { arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt index b2f09024e0..9572b07216 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt @@ -50,6 +50,7 @@ fun openUrlInExternalBrowser(context: Context, uri: Uri?) { uri?.let { val browserIntent = Intent(Intent.ACTION_VIEW, it).apply { putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName) + putExtra(Browser.EXTRA_CREATE_NEW_TAB, true) } try { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 9071b51acf..1e0121a500 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -182,7 +182,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { } } - return true + return super.onOptionsItemSelected(item) } override fun onBackPressed() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt index d42af3e50a..25b526fb8a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt @@ -30,9 +30,9 @@ sealed class RoomDetailActions { data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions() data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions() data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() - data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() + data class SendReaction(val targetEventId: String, val reaction: String) : RoomDetailActions() + data class UndoReaction(val targetEventId: String, val reaction: String, val reason: String? = "") : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() - data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions() data class SetReadMarkerAction(val eventId: String) : RoomDetailActions() @@ -49,6 +49,9 @@ sealed class RoomDetailActions { data class ResendMessage(val eventId: String) : RoomDetailActions() data class RemoveFailedEcho(val eventId: String) : RoomDetailActions() + + data class ReportContent(val eventId: String, val reason: String, val spam: Boolean = false, val inappropriate: Boolean = false) : RoomDetailActions() + object ClearSendQueue : RoomDetailActions() object ResendAll : RoomDetailActions() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 64d50b2804..9aa2f3cccd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.Context +import android.content.DialogInterface import android.content.Intent import android.graphics.drawable.ColorDrawable import android.net.Uri @@ -50,6 +51,7 @@ import com.airbnb.mvrx.* import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.snackbar.Snackbar +import com.google.android.material.textfield.TextInputEditText import com.jaiselrahman.filepicker.activity.FilePickerActivity import com.jaiselrahman.filepicker.config.Configurations import com.jaiselrahman.filepicker.model.MediaFile @@ -68,6 +70,7 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.hideKeyboard @@ -93,8 +96,12 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction +import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.riotx.features.home.room.detail.timeline.item.* +import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.invite.VectorInviteView @@ -264,6 +271,10 @@ class RoomDetailFragment : roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState -> syncStateView.render(syncState) } + + roomDetailViewModel.requestLiveData.observeEvent(this) { + displayRoomDetailActionResult(it) + } } override fun onDestroy() { @@ -420,7 +431,7 @@ class RoomDetailFragment : val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) ?: return // TODO check if already reacted with that? - roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) + roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction)) } } } @@ -766,17 +777,81 @@ class RoomDetailFragment : } private fun displayCommandError(message: String) { - AlertDialog.Builder(activity!!) + AlertDialog.Builder(requireActivity()) .setTitle(R.string.command_error) .setMessage(message) .setPositiveButton(R.string.ok, null) .show() } + private fun promptReasonToReportContent(action: SimpleAction.ReportContentCustom) { + val inflater = requireActivity().layoutInflater + val layout = inflater.inflate(R.layout.dialog_report_content, null) + + val input = layout.findViewById(R.id.dialog_report_content_input) + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.report_content_custom_title) + .setView(layout) + .setPositiveButton(R.string.report_content_custom_submit) { _, _ -> + val reason = input.text.toString() + roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, reason)) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + private fun displayRoomDetailActionResult(result: Async) { + when (result) { + is Fail -> { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(result.error)) + .setPositiveButton(R.string.ok, null) + .show() + } + is Success -> { + when (val data = result.invoke()) { + is RoomDetailActions.ReportContent -> { + when { + data.spam -> { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.content_reported_as_spam_title) + .setMessage(R.string.content_reported_as_spam_content) + .setPositiveButton(R.string.ok, null) + .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") } + .show() + .withColoredButton(DialogInterface.BUTTON_NEGATIVE) + } + data.inappropriate -> { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.content_reported_as_inappropriate_title) + .setMessage(R.string.content_reported_as_inappropriate_content) + .setPositiveButton(R.string.ok, null) + .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") } + .show() + .withColoredButton(DialogInterface.BUTTON_NEGATIVE) + } + else -> { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.content_reported_title) + .setMessage(R.string.content_reported_content) + .setPositiveButton(R.string.ok, null) + .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") } + .show() + .withColoredButton(DialogInterface.BUTTON_NEGATIVE) + } + } + } + } + } + } + } + // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String): Boolean { - return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { + val managed = permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { override fun navToRoom(roomId: String, eventId: String?): Boolean { // Same room? if (roomId == roomDetailArgs.roomId) { @@ -794,6 +869,14 @@ class RoomDetailFragment : return false } }) + + if (!managed) { + // Open in external browser, in a new Tab + openUrlInExternalBrowser(requireContext(), url) + } + + // In fact it is always managed + return true } override fun onUrlLongClicked(url: String): Boolean { @@ -901,7 +984,7 @@ class RoomDetailFragment : override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { if (on) { // we should test the current real state of reaction on this event - roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId)) + roomDetailViewModel.process(RoomDetailActions.SendReaction(informationData.eventId, reaction)) } else { // I need to redact a reaction roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction)) @@ -909,7 +992,7 @@ class RoomDetailFragment : } override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) { - ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } @@ -958,23 +1041,23 @@ class RoomDetailFragment : private fun handleActions(action: SimpleAction) { when (action) { - is SimpleAction.AddReaction -> { + is SimpleAction.AddReaction -> { startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) } - is SimpleAction.ViewReactions -> { - ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) + is SimpleAction.ViewReactions -> { + ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } - is SimpleAction.Copy -> { + is SimpleAction.Copy -> { // I need info about the current selected message :/ copyToClipboard(requireContext(), action.content, false) val msg = requireContext().getString(R.string.copied_to_clipboard) showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) } - is SimpleAction.Delete -> { + is SimpleAction.Delete -> { roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) } - is SimpleAction.Share -> { + is SimpleAction.Share -> { // TODO current data communication is too limited // Need to now the media type // TODO bad, just POC @@ -1002,10 +1085,10 @@ class RoomDetailFragment : } ) } - is SimpleAction.ViewEditHistory -> { + is SimpleAction.ViewEditHistory -> { onEditedDecorationClicked(action.messageInformationData) } - is SimpleAction.ViewSource -> { + is SimpleAction.ViewSource -> { val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) view.findViewById(R.id.event_content_text_view)?.let { it.text = action.content @@ -1016,7 +1099,7 @@ class RoomDetailFragment : .setPositiveButton(R.string.ok, null) .show() } - is SimpleAction.ViewDecryptedSource -> { + is SimpleAction.ViewDecryptedSource -> { val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) view.findViewById(R.id.event_content_text_view)?.let { it.text = action.content @@ -1027,31 +1110,40 @@ class RoomDetailFragment : .setPositiveButton(R.string.ok, null) .show() } - is SimpleAction.QuickReact -> { + is SimpleAction.QuickReact -> { // eventId,ClickedOn,Add roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } - is SimpleAction.Edit -> { + is SimpleAction.Edit -> { roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString())) } - is SimpleAction.Quote -> { + is SimpleAction.Quote -> { roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString())) } - is SimpleAction.Reply -> { + is SimpleAction.Reply -> { roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString())) } - is SimpleAction.CopyPermalink -> { + is SimpleAction.CopyPermalink -> { val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) } - is SimpleAction.Resend -> { + is SimpleAction.Resend -> { roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId)) } - is SimpleAction.Remove -> { + is SimpleAction.Remove -> { roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId)) } - else -> { + is SimpleAction.ReportContentSpam -> { + roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is spam", spam = true)) + } + is SimpleAction.ReportContentInappropriate -> { + roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is inappropriate", inappropriate = true)) + } + is SimpleAction.ReportContentCustom -> { + promptReasonToReportContent(action) + } + else -> { Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index a9e13df859..4d93c8a16c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -20,10 +20,7 @@ import android.net.Uri import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.* import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -96,6 +93,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private var timeline = room.createTimeline(eventId, timelineSettings) + // Can be used for several actions, for a one shot result + private val _requestLiveData = MutableLiveData>>() + val requestLiveData: LiveData>> + get() = _requestLiveData + // Slot to keep a pending action during permission request var pendingAction: RoomDetailActions? = null @@ -154,6 +156,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailActions.ResendAll -> handleResendAll() is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action) is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailActions.ReportContent -> handleReportContent(action) } } @@ -447,7 +450,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleSendReaction(action: RoomDetailActions.SendReaction) { - room.sendReaction(action.reaction, action.targetEventId) + room.sendReaction(action.targetEventId, action.reaction) } private fun handleRedactEvent(action: RoomDetailActions.RedactAction) { @@ -456,14 +459,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleUndoReact(action: RoomDetailActions.UndoReaction) { - room.undoReaction(action.key, action.targetEventId, session.myUserId) + room.undoReaction(action.targetEventId, action.reaction) } private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) { if (action.add) { - room.sendReaction(action.selectedReaction, action.targetEventId) + room.sendReaction(action.targetEventId, action.selectedReaction) } else { - room.undoReaction(action.selectedReaction, action.targetEventId, session.myUserId) + room.undoReaction(action.targetEventId, action.selectedReaction) } } @@ -710,6 +713,18 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro room.markAllAsRead(object : MatrixCallback {}) } + private fun handleReportContent(action: RoomDetailActions.ReportContent) { + room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback { + override fun onSuccess(data: Unit) { + _requestLiveData.postValue(LiveEvent(Success(action))) + } + + override fun onFailure(failure: Throwable) { + _requestLiveData.postValue(LiveEvent(Fail(failure))) + } + }) + } + private fun observeSyncState() { session.rx() .liveSyncState() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt index 510fc1e26f..50ade56474 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt @@ -21,19 +21,18 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout -import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife -import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.args import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import kotlinx.android.parcel.Parcelize -import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* +import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.* import javax.inject.Inject @Parcelize @@ -48,8 +47,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { @Inject lateinit var epoxyController: DisplayReadReceiptsController - @BindView(R.id.bottom_sheet_display_reactions_list) - lateinit var epoxyRecyclerView: EpoxyRecyclerView + @BindView(R.id.bottomSheetRecyclerView) + lateinit var recyclerView: RecyclerView private val displayReadReceiptArgs: DisplayReadReceiptArgs by args() @@ -58,24 +57,20 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false) + val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false) ButterKnife.bind(this, view) return view } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - epoxyRecyclerView.setController(epoxyController) - val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, - LinearLayout.VERTICAL) - epoxyRecyclerView.addItemDecoration(dividerItemDecoration) + recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + recyclerView.adapter = epoxyController.adapter bottomSheetTitle.text = getString(R.string.read_at) epoxyController.setData(displayReadReceiptArgs.readReceipts) } - override fun invalidate() { - // we are not using state for this one as it's static - } + // we are not using state for this one as it's static, so no need to override invalidate() companion object { fun newInstance(readReceipts: List): DisplayReadReceiptsBottomSheet { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt new file mode 100644 index 0000000000..d0d5b1deea --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +/** + * A action for bottom sheet. + */ +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_action) +abstract class BottomSheetItemAction : VectorEpoxyModel() { + + @EpoxyAttribute + @DrawableRes + var iconRes: Int = 0 + @EpoxyAttribute + var textRes: Int = 0 + @EpoxyAttribute + var showExpand = false + @EpoxyAttribute + var expanded = false + @EpoxyAttribute + var subMenuItem = false + @EpoxyAttribute + lateinit var listener: View.OnClickListener + + override fun bind(holder: Holder) { + holder.view.setOnClickListener { + listener.onClick(it) + } + + holder.startSpace.isVisible = subMenuItem + holder.icon.setImageResource(iconRes) + holder.text.setText(textRes) + holder.expand.isVisible = showExpand + if (showExpand) { + holder.expand.setImageResource(if (expanded) R.drawable.ic_material_expand_less_black else R.drawable.ic_material_expand_more_black) + } + } + + class Holder : VectorEpoxyHolder() { + val startSpace by bind(R.id.action_start_space) + val icon by bind(R.id.action_icon) + val text by bind(R.id.action_title) + val expand by bind(R.id.action_expand) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt new file mode 100644 index 0000000000..d37aa43770 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action + +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData + +/** + * A message preview for bottom sheet. + */ +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview) +abstract class BottomSheetItemMessagePreview : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute + lateinit var informationData: MessageInformationData + @EpoxyAttribute + var senderName: String? = null + @EpoxyAttribute + lateinit var body: CharSequence + @EpoxyAttribute + var time: CharSequence? = null + + override fun bind(holder: Holder) { + avatarRenderer.render(informationData.avatarUrl, informationData.senderId, senderName, holder.avatar) + holder.sender.setTextOrHide(senderName) + holder.body.text = body + holder.timestamp.setTextOrHide(time) + } + + class Holder : VectorEpoxyHolder() { + val avatar by bind(R.id.bottom_sheet_message_preview_avatar) + val sender by bind(R.id.bottom_sheet_message_preview_sender) + val body by bind(R.id.bottom_sheet_message_preview_body) + val timestamp by bind(R.id.bottom_sheet_message_preview_timestamp) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt new file mode 100644 index 0000000000..3aafa7c974 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemQuickReactions.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action + +import android.graphics.Typeface +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +/** + * A quick reaction list for bottom sheet. + */ +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction) +abstract class BottomSheetItemQuickReactions : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var fontProvider: EmojiCompatFontProvider + @EpoxyAttribute + lateinit var texts: List + @EpoxyAttribute + lateinit var selecteds: List + @EpoxyAttribute + var listener: Listener? = null + + override fun bind(holder: Holder) { + holder.textViews.forEachIndexed { index, textView -> + textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT + textView.text = texts[index] + textView.alpha = if (selecteds[index]) 0.2f else 1f + + textView.setOnClickListener { + listener?.didSelect(texts[index], !selecteds[index]) + } + } + } + + class Holder : VectorEpoxyHolder() { + private val quickReaction0 by bind(R.id.quickReaction0) + private val quickReaction1 by bind(R.id.quickReaction1) + private val quickReaction2 by bind(R.id.quickReaction2) + private val quickReaction3 by bind(R.id.quickReaction3) + private val quickReaction4 by bind(R.id.quickReaction4) + private val quickReaction5 by bind(R.id.quickReaction5) + private val quickReaction6 by bind(R.id.quickReaction6) + private val quickReaction7 by bind(R.id.quickReaction7) + + val textViews + get() = listOf( + quickReaction0, + quickReaction1, + quickReaction2, + quickReaction3, + quickReaction4, + quickReaction5, + quickReaction6, + quickReaction7 + ) + } + + interface Listener { + fun didSelect(emoji: String, selected: Boolean) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt new file mode 100644 index 0000000000..86a5512349 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action + +import android.view.View +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +/** + * A send state for bottom sheet. + */ +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status) +abstract class BottomSheetItemSendState : VectorEpoxyModel() { + + @EpoxyAttribute + var showProgress: Boolean = false + @EpoxyAttribute + lateinit var text: CharSequence + @EpoxyAttribute + @DrawableRes + var drawableStart: Int = 0 + + override fun bind(holder: Holder) { + holder.progress.isVisible = showProgress + holder.text.setCompoundDrawablesWithIntrinsicBounds(drawableStart, 0, 0, 0) + holder.text.text = text + } + + class Holder : VectorEpoxyHolder() { + val progress by bind(R.id.messageStatusProgress) + val text by bind(R.id.messageStatusText) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt new file mode 100644 index 0000000000..f09f68b714 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action + +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider) +abstract class BottomSheetItemSeparator : VectorEpoxyModel() { + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 11f3207e32..8aaa7643c2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -15,62 +15,46 @@ */ package im.vector.riotx.features.home.room.detail.timeline.action -import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView -import android.widget.TextView -import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife -import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import kotlinx.android.synthetic.main.bottom_sheet_message_actions.* import javax.inject.Inject /** * Bottom sheet fragment that shows a message preview with list of contextual actions - * (Includes fragments for quick reactions and list of actions) */ -class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { +class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), MessageActionsEpoxyController.MessageActionsEpoxyControllerListener { + + @Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory + @Inject lateinit var messageActionsEpoxyController: MessageActionsEpoxyController + + @BindView(R.id.bottomSheetRecyclerView) + lateinit var recyclerView: RecyclerView - @Inject - lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory - @Inject - lateinit var avatarRenderer: AvatarRenderer private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class) + override val showExpanded = true + private lateinit var actionHandlerModel: ActionsHandler - @BindView(R.id.bottom_sheet_message_preview_avatar) - lateinit var senderAvatarImageView: ImageView - - @BindView(R.id.bottom_sheet_message_preview_sender) - lateinit var senderNameTextView: TextView - - @BindView(R.id.bottom_sheet_message_preview_timestamp) - lateinit var messageTimestampText: TextView - - @BindView(R.id.bottom_sheet_message_preview_body) - lateinit var messageBodyTextView: TextView - override fun injectWith(screenComponent: ScreenComponent) { screenComponent.inject(this) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view = inflater.inflate(R.layout.bottom_sheet_message_actions, container, false) + val view = inflater.inflate(R.layout.bottom_sheet_generic_list, container, false) ButterKnife.bind(this, view) return view } @@ -78,78 +62,26 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) - - val cfm = childFragmentManager - var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment - if (menuActionFragment == null) { - menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs) - cfm.beginTransaction() - .replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment") - .commit() - } - menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener { - override fun didSelectMenuAction(simpleAction: SimpleAction) { - actionHandlerModel.fireAction(simpleAction) - dismiss() - } - } - - var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment - if (quickReactionFragment == null) { - quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs) - cfm.beginTransaction() - .replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction") - .commit() - } - quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener { - override fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) { - actionHandlerModel.fireAction(SimpleAction.QuickReact(eventId, clickedOn, add)) - dismiss() - } - } + recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + recyclerView.adapter = messageActionsEpoxyController.adapter + // Disable item animation + recyclerView.itemAnimator = null + messageActionsEpoxyController.listener = this } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val dialog = super.onCreateDialog(savedInstanceState) - // We want to force the bottom sheet initial state to expanded - (dialog as? BottomSheetDialog)?.let { bottomSheetDialog -> - bottomSheetDialog.setOnShowListener { dialog -> - val d = dialog as BottomSheetDialog - (d.findViewById(com.google.android.material.R.id.design_bottom_sheet) as? FrameLayout)?.let { - BottomSheetBehavior.from(it).state = BottomSheetBehavior.STATE_COLLAPSED - } - } + override fun didSelectMenuAction(simpleAction: SimpleAction) { + if (simpleAction is SimpleAction.ReportContent) { + // Toggle report menu + viewModel.toggleReportMenu() + } else { + actionHandlerModel.fireAction(simpleAction) + dismiss() } - return dialog } override fun invalidate() = withState(viewModel) { - val body = viewModel.resolveBody(it) - if (body != null) { - bottom_sheet_message_preview.isVisible = true - senderNameTextView.text = it.senderName() - messageBodyTextView.text = body - messageTimestampText.text = it.time() - avatarRenderer.render(it.informationData.avatarUrl, it.informationData.senderId, it.senderName(), senderAvatarImageView) - } else { - bottom_sheet_message_preview.isVisible = false - } - quickReactBottomDivider.isVisible = it.canReact() - bottom_sheet_quick_reaction_container.isVisible = it.canReact() - if (it.informationData.sendState.isSending()) { - messageStatusInfo.isVisible = true - messageStatusProgress.isVisible = true - messageStatusText.text = getString(R.string.event_status_sending_message) - messageStatusText.setCompoundDrawables(null, null, null, null) - } else if (it.informationData.sendState.hasFailed()) { - messageStatusInfo.isVisible = true - messageStatusProgress.isVisible = false - messageStatusText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_warning_small, 0, 0, 0) - messageStatusText.text = getString(R.string.unable_to_send_message) - } else { - messageStatusInfo.isVisible = false - } - return@withState + messageActionsEpoxyController.setData(it) + super.invalidate() } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt new file mode 100644 index 0000000000..d9119f08b3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Success +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +/** + * Epoxy controller for message action list + */ +class MessageActionsEpoxyController @Inject constructor(private val stringProvider: StringProvider, + private val avatarRenderer: AvatarRenderer, + private val fontProvider: EmojiCompatFontProvider) : TypedEpoxyController() { + + var listener: MessageActionsEpoxyControllerListener? = null + + override fun buildModels(state: MessageActionState) { + // Message preview + val body = state.messageBody + if (body != null) { + bottomSheetItemMessagePreview { + id("preview") + avatarRenderer(avatarRenderer) + informationData(state.informationData) + senderName(state.senderName()) + body(body) + time(state.time()) + } + } + + // Send state + if (state.informationData.sendState.isSending()) { + bottomSheetItemSendState { + id("send_state") + showProgress(true) + text(stringProvider.getString(R.string.event_status_sending_message)) + } + } else if (state.informationData.sendState.hasFailed()) { + bottomSheetItemSendState { + id("send_state") + showProgress(false) + text(stringProvider.getString(R.string.unable_to_send_message)) + drawableStart(R.drawable.ic_warning_small) + } + } + + // Quick reactions + if (state.canReact() && state.quickStates is Success) { + // Separator + bottomSheetItemSeparator { + id("reaction_separator") + } + + bottomSheetItemQuickReactions { + id("quick_reaction") + fontProvider(fontProvider) + texts(state.quickStates()?.map { it.reaction }.orEmpty()) + selecteds(state.quickStates.invoke().map { it.isSelected }) + listener(object : BottomSheetItemQuickReactions.Listener { + override fun didSelect(emoji: String, selected: Boolean) { + listener?.didSelectMenuAction(SimpleAction.QuickReact(state.eventId, emoji, selected)) + } + }) + } + } + + // Separator + bottomSheetItemSeparator { + id("actions_separator") + } + + // Action + state.actions()?.forEachIndexed { index, action -> + bottomSheetItemAction { + id("action_$index") + iconRes(action.iconResId) + textRes(action.titleRes) + showExpand(action is SimpleAction.ReportContent) + expanded(state.expendedReportContentMenu) + listener(View.OnClickListener { listener?.didSelectMenuAction(action) }) + } + + if (action is SimpleAction.ReportContent && state.expendedReportContentMenu) { + // Special case for report content menu: add the submenu + listOf( + SimpleAction.ReportContentSpam(action.eventId), + SimpleAction.ReportContentInappropriate(action.eventId), + SimpleAction.ReportContentCustom(action.eventId) + ).forEachIndexed { indexReport, actionReport -> + bottomSheetItemAction { + id("actionReport_$indexReport") + subMenuItem(true) + iconRes(actionReport.iconResId) + textRes(actionReport.titleRes) + listener(View.OnClickListener { listener?.didSelectMenuAction(actionReport) }) + } + } + } + } + } + + interface MessageActionsEpoxyControllerListener { + fun didSelectMenuAction(simpleAction: SimpleAction) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index c80c7c5f15..3b25a9e908 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -21,26 +21,48 @@ import com.squareup.inject.assisted.AssistedInject import dagger.Lazy import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.isTextMessage +import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent +import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited +import im.vector.matrix.android.api.util.Optional import im.vector.matrix.rx.RxRoom import im.vector.matrix.rx.unwrap +import im.vector.riotx.R import im.vector.riotx.core.extensions.canReact import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.html.EventHtmlRenderer import java.text.SimpleDateFormat import java.util.* +/** + * Quick reactions state + */ +data class ToggleState( + val reaction: String, + val isSelected: Boolean +) + data class MessageActionState( val roomId: String, val eventId: String, val informationData: MessageInformationData, - val timelineEvent: Async = Uninitialized + val timelineEvent: Async = Uninitialized, + val messageBody: CharSequence? = null, + // For quick reactions + val quickStates: Async> = Uninitialized, + // For actions + val actions: Async> = Uninitialized, + val expendedReportContentMenu: Boolean = false ) : MvRxState { constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) @@ -49,18 +71,101 @@ data class MessageActionState( fun senderName(): String = informationData.memberName?.toString() ?: "" - fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } - ?: "" + fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: "" fun canReact() = timelineEvent()?.canReact() == true +} - fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? { +/** + * Information related to an event and used to display preview in contextual bottomsheet. + */ +class MessageActionsViewModel @AssistedInject constructor(@Assisted + initialState: MessageActionState, + private val eventHtmlRenderer: Lazy, + private val session: Session, + private val noticeEventFormatter: NoticeEventFormatter, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { + + private val eventId = initialState.eventId + private val informationData = initialState.informationData + private val room = session.getRoom(initialState.roomId) + + @AssistedInject.Factory + interface Factory { + fun create(initialState: MessageActionState): MessageActionsViewModel + } + + companion object : MvRxViewModelFactory { + + val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀") + + override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { + val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.messageActionViewModelFactory.create(state) + } + } + + init { + observeEvent() + observeReactions() + observeEventAction() + } + + fun toggleReportMenu() = withState { + setState { + copy( + expendedReportContentMenu = it.expendedReportContentMenu.not() + ) + } + } + + private fun observeEvent() { + if (room == null) return + RxRoom(room) + .liveTimelineEvent(eventId) + .unwrap() + .execute { + copy( + timelineEvent = it, + messageBody = computeMessageBody(it) + ) + } + } + + private fun observeEventAction() { + if (room == null) return + RxRoom(room) + .liveTimelineEvent(eventId) + .map { + actionsForEvent(it) + } + .execute { + copy(actions = it) + } + } + + private fun observeReactions() { + if (room == null) return + RxRoom(room) + .liveAnnotationSummary(eventId) + .map { annotations -> + quickEmojis.map { emoji -> + ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false) + } + } + .execute { + copy(quickStates = it) + } + } + + private fun computeMessageBody(timelineEvent: Async): CharSequence? { return when (timelineEvent()?.root?.getClearType()) { EventType.MESSAGE -> { val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent() if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { - eventHtmlRenderer?.render(messageContent.formattedBody - ?: messageContent.body) + eventHtmlRenderer.get().render(messageContent.formattedBody + ?: messageContent.body) } else { messageContent?.body } @@ -72,54 +177,177 @@ data class MessageActionState( EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> { - timelineEvent()?.let { noticeEventFormatter?.format(it) } + timelineEvent()?.let { noticeEventFormatter.format(it) } } else -> null } } -} -/** - * Information related to an event and used to display preview in contextual bottomsheet. - */ -class MessageActionsViewModel @AssistedInject constructor(@Assisted - initialState: MessageActionState, - private val eventHtmlRenderer: Lazy, - session: Session, - private val noticeEventFormatter: NoticeEventFormatter -) : VectorViewModel(initialState) { + private fun actionsForEvent(optionalEvent: Optional): List { + val event = optionalEvent.getOrNull() ?: return emptyList() - private val eventId = initialState.eventId - private val room = session.getRoom(initialState.roomId) + val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel() + ?: event.root.getClearContent().toModel() + val type = messageContent?.type - @AssistedInject.Factory - interface Factory { - fun create(initialState: MessageActionState): MessageActionsViewModel - } + return arrayListOf().apply { + if (event.root.sendState.hasFailed()) { + if (canRetry(event)) { + add(SimpleAction.Resend(eventId)) + } + add(SimpleAction.Remove(eventId)) + } else if (event.root.sendState.isSending()) { + // TODO is uploading attachment? + if (canCancel(event)) { + add(SimpleAction.Cancel(eventId)) + } + } else { + if (!event.root.isRedacted()) { + if (canReply(event, messageContent)) { + add(SimpleAction.Reply(eventId)) + } - companion object : MvRxViewModelFactory { + if (canEdit(event, session.myUserId)) { + add(SimpleAction.Edit(eventId)) + } - override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { - val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.messageActionViewModelFactory.create(state) + if (canRedact(event, session.myUserId)) { + add(SimpleAction.Delete(eventId)) + } + + if (canCopy(type)) { + // TODO copy images? html? see ClipBoard + add(SimpleAction.Copy(messageContent!!.body)) + } + + if (event.canReact()) { + add(SimpleAction.AddReaction(eventId)) + } + + if (canQuote(event, messageContent)) { + add(SimpleAction.Quote(eventId)) + } + + if (canViewReactions(event)) { + add(SimpleAction.ViewReactions(informationData)) + } + + if (event.hasBeenEdited()) { + add(SimpleAction.ViewEditHistory(informationData)) + } + + if (canShare(type)) { + if (messageContent is MessageImageContent) { + session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url -> + add(SimpleAction.Share(url)) + } + } + // TODO + } + + if (event.root.sendState == SendState.SENT) { + // TODO Can be redacted + + // TODO sent by me or sufficient power level + } + } + + add(SimpleAction.ViewSource(event.root.toContentStringWithIndent())) + if (event.isEncrypted()) { + val decryptedContent = event.root.toClearContentStringWithIndent() + ?: stringProvider.getString(R.string.encryption_information_decryption_error) + add(SimpleAction.ViewDecryptedSource(decryptedContent)) + } + add(SimpleAction.CopyPermalink(eventId)) + + if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) { + // not sent by me + add(SimpleAction.ReportContent(eventId)) + } + } } } - init { - observeEvent() + private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean { + return false } - private fun observeEvent() { - if (room == null) return - RxRoom(room) - .liveTimelineEvent(eventId) - .unwrap() - .execute { - copy(timelineEvent = it) - } + private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean { + // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + return when (messageContent?.type) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_FILE -> true + else -> false + } } - fun resolveBody(state: MessageActionState): CharSequence? { - return state.messageBody(eventHtmlRenderer.get(), noticeEventFormatter) + private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean { + // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + return when (messageContent?.type) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.FORMAT_MATRIX_HTML, + MessageType.MSGTYPE_LOCATION -> { + true + } + else -> false + } + } + + private fun canRedact(event: TimelineEvent, myUserId: String): Boolean { + // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + // TODO if user is admin or moderator + return event.root.senderId == myUserId + } + + private fun canRetry(event: TimelineEvent): Boolean { + return event.root.sendState.hasFailed() && event.root.isTextMessage() + } + + private fun canViewReactions(event: TimelineEvent): Boolean { + // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + // TODO if user is admin or moderator + return event.annotations?.reactionsSummary?.isNotEmpty() ?: false + } + + private fun canEdit(event: TimelineEvent, myUserId: String): Boolean { + // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.getClearType() != EventType.MESSAGE) return false + // TODO if user is admin or moderator + val messageContent = event.root.getClearContent().toModel() + return event.root.senderId == myUserId && ( + messageContent?.type == MessageType.MSGTYPE_TEXT + || messageContent?.type == MessageType.MSGTYPE_EMOTE + ) + } + + private fun canCopy(type: String?): Boolean { + return when (type) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.FORMAT_MATRIX_HTML, + MessageType.MSGTYPE_LOCATION -> true + else -> false + } + } + + private fun canShare(type: String?): Boolean { + return when (type) { + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_VIDEO -> true + else -> false + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt deleted file mode 100644 index 2eec705eea..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuFragment.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.home.room.detail.timeline.action - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState -import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.themes.ThemeUtils -import javax.inject.Inject - -/** - * Fragment showing the list of available contextual action for a given message. - */ -class MessageMenuFragment : VectorBaseFragment() { - - @Inject lateinit var messageMenuViewModelFactory: MessageMenuViewModel.Factory - private val viewModel: MessageMenuViewModel by fragmentViewModel(MessageMenuViewModel::class) - private var addSeparators = false - var interactionListener: InteractionListener? = null - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun getLayoutResId() = R.layout.fragment_message_menu - - override fun invalidate() = withState(viewModel) { state -> - - val linearLayout = view as? LinearLayout - if (linearLayout != null) { - val inflater = LayoutInflater.from(linearLayout.context) - linearLayout.removeAllViews() - var insertIndex = 0 - val actions = state.actions() - actions?.forEachIndexed { index, action -> - inflateActionView(action, inflater, linearLayout)?.let { - it.setOnClickListener { - interactionListener?.didSelectMenuAction(action) - } - linearLayout.addView(it, insertIndex) - insertIndex++ - if (addSeparators) { - if (index < actions.size - 1) { - linearLayout.addView(inflateSeparatorView(), insertIndex) - insertIndex++ - } - } - } - } - } - } - - private fun inflateActionView(action: SimpleAction, inflater: LayoutInflater, container: ViewGroup?): View? { - return inflater.inflate(R.layout.adapter_item_action, container, false)?.apply { - findViewById(R.id.action_icon)?.setImageResource(action.iconResId) - findViewById(R.id.action_title)?.setText(action.titleRes) - } - } - - private fun inflateSeparatorView(): View { - val frame = FrameLayout(requireContext()) - frame.setBackgroundColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_list_divider_color)) - frame.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, requireContext().resources.displayMetrics.density.toInt()) - return frame - } - - interface InteractionListener { - fun didSelectMenuAction(simpleAction: SimpleAction) - } - - companion object { - fun newInstance(pa: TimelineEventFragmentArgs): MessageMenuFragment { - val args = Bundle() - args.putParcelable(MvRx.KEY_ARG, pa) - val fragment = MessageMenuFragment() - fragment.arguments = args - return fragment - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt deleted file mode 100644 index 14d730044a..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.home.room.detail.timeline.action - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import com.airbnb.mvrx.* -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.isTextMessage -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageType -import im.vector.matrix.android.api.session.room.send.SendState -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited -import im.vector.matrix.android.api.util.Optional -import im.vector.matrix.rx.RxRoom -import im.vector.riotx.R -import im.vector.riotx.core.extensions.canReact -import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData - -sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) { - data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction) - data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy) - data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit) - data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote) - data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply) - data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share) - data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw) - data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash) - data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete) - data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round) - data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source) - data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source) - data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink) - data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag) - data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0) - data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions) - data class ViewEditHistory(val messageInformationData: MessageInformationData) : - SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history) -} - -data class MessageMenuState( - val roomId: String, - val eventId: String, - val informationData: MessageInformationData, - val actions: Async> = Uninitialized -) : MvRxState { - - constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) -} - -/** - * Manages list actions for a given message (copy / paste / forward...) - */ -class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: MessageMenuState, - private val session: Session, - private val stringProvider: StringProvider) : VectorViewModel(initialState) { - - @AssistedInject.Factory - interface Factory { - fun create(initialState: MessageMenuState): MessageMenuViewModel - } - - private val room = session.getRoom(initialState.roomId) - ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") - - private val eventId = initialState.eventId - private val informationData: MessageInformationData = initialState.informationData - - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: MessageMenuState): MessageMenuViewModel? { - val fragment: MessageMenuFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.messageMenuViewModelFactory.create(state) - } - } - - init { - observeEvent() - } - - private fun observeEvent() { - RxRoom(room) - .liveTimelineEvent(eventId) - .map { - actionsForEvent(it) - } - .execute { - copy(actions = it) - } - } - - private fun actionsForEvent(optionalEvent: Optional): List { - val event = optionalEvent.getOrNull() ?: return emptyList() - - val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel() - ?: event.root.getClearContent().toModel() - val type = messageContent?.type - - return arrayListOf().apply { - if (event.root.sendState.hasFailed()) { - if (canRetry(event)) { - add(SimpleAction.Resend(eventId)) - } - add(SimpleAction.Remove(eventId)) - } else if (event.root.sendState.isSending()) { - // TODO is uploading attachment? - if (canCancel(event)) { - add(SimpleAction.Cancel(eventId)) - } - } else { - if (!event.root.isRedacted()) { - if (canReply(event, messageContent)) { - add(SimpleAction.Reply(eventId)) - } - - if (canEdit(event, session.myUserId)) { - add(SimpleAction.Edit(eventId)) - } - - if (canRedact(event, session.myUserId)) { - add(SimpleAction.Delete(eventId)) - } - - if (canCopy(type)) { - // TODO copy images? html? see ClipBoard - add(SimpleAction.Copy(messageContent!!.body)) - } - - if (event.canReact()) { - add(SimpleAction.AddReaction(eventId)) - } - - if (canQuote(event, messageContent)) { - add(SimpleAction.Quote(eventId)) - } - - if (canViewReactions(event)) { - add(SimpleAction.ViewReactions(informationData)) - } - - if (event.hasBeenEdited()) { - add(SimpleAction.ViewEditHistory(informationData)) - } - - if (canShare(type)) { - if (messageContent is MessageImageContent) { - session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url -> - add(SimpleAction.Share(url)) - } - } - // TODO - } - - if (event.root.sendState == SendState.SENT) { - // TODO Can be redacted - - // TODO sent by me or sufficient power level - } - } - - add(SimpleAction.ViewSource(event.root.toContentStringWithIndent())) - if (event.isEncrypted()) { - val decryptedContent = event.root.toClearContentStringWithIndent() - ?: stringProvider.getString(R.string.encryption_information_decryption_error) - add(SimpleAction.ViewDecryptedSource(decryptedContent)) - } - add(SimpleAction.CopyPermalink(eventId)) - - if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) { - // not sent by me - add(SimpleAction.Flag(eventId)) - } - } - } - } - - private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean { - return false - } - - private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean { - // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false - return when (messageContent?.type) { - MessageType.MSGTYPE_TEXT, - MessageType.MSGTYPE_NOTICE, - MessageType.MSGTYPE_EMOTE, - MessageType.MSGTYPE_IMAGE, - MessageType.MSGTYPE_VIDEO, - MessageType.MSGTYPE_AUDIO, - MessageType.MSGTYPE_FILE -> true - else -> false - } - } - - private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean { - // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false - return when (messageContent?.type) { - MessageType.MSGTYPE_TEXT, - MessageType.MSGTYPE_NOTICE, - MessageType.MSGTYPE_EMOTE, - MessageType.FORMAT_MATRIX_HTML, - MessageType.MSGTYPE_LOCATION -> { - true - } - else -> false - } - } - - private fun canRedact(event: TimelineEvent, myUserId: String): Boolean { - // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false - // TODO if user is admin or moderator - return event.root.senderId == myUserId - } - - private fun canRetry(event: TimelineEvent): Boolean { - return event.root.sendState.hasFailed() && event.root.isTextMessage() - } - - private fun canViewReactions(event: TimelineEvent): Boolean { - // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false - // TODO if user is admin or moderator - return event.annotations?.reactionsSummary?.isNotEmpty() ?: false - } - - private fun canEdit(event: TimelineEvent, myUserId: String): Boolean { - // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false - // TODO if user is admin or moderator - val messageContent = event.root.getClearContent().toModel() - return event.root.senderId == myUserId && ( - messageContent?.type == MessageType.MSGTYPE_TEXT - || messageContent?.type == MessageType.MSGTYPE_EMOTE - ) - } - - private fun canCopy(type: String?): Boolean { - return when (type) { - MessageType.MSGTYPE_TEXT, - MessageType.MSGTYPE_NOTICE, - MessageType.MSGTYPE_EMOTE, - MessageType.FORMAT_MATRIX_HTML, - MessageType.MSGTYPE_LOCATION -> true - else -> false - } - } - - private fun canShare(type: String?): Boolean { - return when (type) { - MessageType.MSGTYPE_IMAGE, - MessageType.MSGTYPE_AUDIO, - MessageType.MSGTYPE_VIDEO -> true - else -> false - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt deleted file mode 100644 index cabb4c113f..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionFragment.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.home.room.detail.timeline.action - -import android.graphics.Typeface -import android.os.Bundle -import android.view.View -import android.widget.TextView -import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState -import im.vector.riotx.EmojiCompatFontProvider -import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.platform.VectorBaseFragment -import kotlinx.android.synthetic.main.adapter_item_action_quick_reaction.* -import javax.inject.Inject - -/** - * Quick Reaction Fragment (agree / like reactions) - */ -class QuickReactionFragment : VectorBaseFragment() { - - private val viewModel: QuickReactionViewModel by fragmentViewModel(QuickReactionViewModel::class) - - var interactionListener: InteractionListener? = null - - @Inject lateinit var fontProvider: EmojiCompatFontProvider - @Inject lateinit var quickReactionViewModelFactory: QuickReactionViewModel.Factory - - override fun getLayoutResId() = R.layout.adapter_item_action_quick_reaction - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - private lateinit var textViews: List - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - textViews = listOf(quickReaction0, quickReaction1, quickReaction2, quickReaction3, - quickReaction4, quickReaction5, quickReaction6, quickReaction7) - textViews.forEachIndexed { index, textView -> - textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT - textView.setOnClickListener { - viewModel.didSelect(index) - } - } - } - - override fun invalidate() = withState(viewModel) { - val quickReactionsStates = it.quickStates() ?: return@withState - quickReactionsStates.forEachIndexed { index, qs -> - textViews[index].text = qs.reaction - textViews[index].alpha = if (qs.isSelected) 0.2f else 1f - } - - if (it.result != null) { - interactionListener?.didQuickReactWith(it.result.reaction, it.result.isSelected, it.eventId) - } - } - - interface InteractionListener { - fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) - } - - companion object { - fun newInstance(pa: TimelineEventFragmentArgs): QuickReactionFragment { - val args = Bundle() - args.putParcelable(MvRx.KEY_ARG, pa) - val fragment = QuickReactionFragment() - fragment.arguments = args - return fragment - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt deleted file mode 100644 index edcfd8e28c..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.home.room.detail.timeline.action - -import com.airbnb.mvrx.* -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.rx.RxRoom -import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData - -/** - * Quick reactions state, it's a toggle with 3rd state - */ -data class ToggleState( - val reaction: String, - val isSelected: Boolean -) - -data class QuickReactionState( - val roomId: String, - val eventId: String, - val informationData: MessageInformationData, - val quickStates: Async> = Uninitialized, - val result: ToggleState? = null - /** Pair of 'clickedOn' and current toggles state*/ -) : MvRxState { - - constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) -} - -/** - * Quick reaction view model - */ -class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState: QuickReactionState, - private val session: Session) : VectorViewModel(initialState) { - - @AssistedInject.Factory - interface Factory { - fun create(initialState: QuickReactionState): QuickReactionViewModel - } - - private val room = session.getRoom(initialState.roomId) - private val eventId = initialState.eventId - - companion object : MvRxViewModelFactory { - - val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀") - - override fun create(viewModelContext: ViewModelContext, state: QuickReactionState): QuickReactionViewModel? { - val fragment: QuickReactionFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.quickReactionViewModelFactory.create(state) - } - } - - init { - observeReactions() - } - - private fun observeReactions() { - if (room == null) return - RxRoom(room) - .liveAnnotationSummary(eventId) - .map { annotations -> - quickEmojis.map { emoji -> - ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe - ?: false) - } - } - .execute { - copy(quickStates = it) - } - } - - fun didSelect(index: Int) = withState { - val selectedReaction = it.quickStates()?.get(index) ?: return@withState - val isSelected = selectedReaction.isSelected - setState { - copy(result = ToggleState(selectedReaction.reaction, !isSelected)) - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt new file mode 100644 index 0000000000..5da589d862 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 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.riotx.features.home.room.detail.timeline.action + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import im.vector.riotx.R +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData + +sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) { + data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction) + data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy) + data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit) + data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote) + data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply) + data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share) + data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw) + data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash) + data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete) + data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round) + data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source) + data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source) + data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink) + data class ReportContent(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag) + data class ReportContentSpam(val eventId: String) : SimpleAction(R.string.report_content_spam, R.drawable.ic_report_spam) + data class ReportContentInappropriate(val eventId: String) : SimpleAction(R.string.report_content_inappropriate, R.drawable.ic_report_inappropriate) + data class ReportContentCustom(val eventId: String) : SimpleAction(R.string.report_content_custom, R.drawable.ic_report_custom) + data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0) + data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions) + data class ViewEditHistory(val messageInformationData: MessageInformationData) : + SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history) +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt similarity index 75% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt index 5fefb36e29..709bcb53c7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.edithistory import android.os.Bundle import android.view.LayoutInflater @@ -21,17 +21,20 @@ import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife -import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.html.EventHtmlRenderer -import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* +import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.* import javax.inject.Inject /** @@ -44,8 +47,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() { @Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer - @BindView(R.id.bottom_sheet_display_reactions_list) - lateinit var epoxyRecyclerView: EpoxyRecyclerView + @BindView(R.id.bottomSheetRecyclerView) + lateinit var recyclerView: RecyclerView private val epoxyController by lazy { ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer) @@ -56,22 +59,23 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false) + val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false) ButterKnife.bind(this, view) return view } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - epoxyRecyclerView.setController(epoxyController) - val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, - LinearLayout.VERTICAL) - epoxyRecyclerView.addItemDecoration(dividerItemDecoration) + recyclerView.adapter = epoxyController.adapter + recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL) + recyclerView.addItemDecoration(dividerItemDecoration) bottomSheetTitle.text = context?.getString(R.string.message_edits) } override fun invalidate() = withState(viewModel) { epoxyController.setData(it) + super.invalidate() } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt similarity index 98% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index 288f001651..d36e98f67c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.edithistory import android.content.Context import android.text.Spannable @@ -41,7 +41,7 @@ import timber.log.Timber import java.util.* /** - * Epoxy controller for reaction event list + * Epoxy controller for edit history list */ class ViewEditHistoryEpoxyController(private val context: Context, val dateFormatter: VectorDateFormatter, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt index 890fbe60e5..e2b976b273 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.edithistory import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import timber.log.Timber import java.util.* diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 58bda1eaf5..0bb5c3a1d8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -19,9 +19,9 @@ package im.vector.riotx.features.home.room.detail.timeline.factory import android.text.SpannableStringBuilder import android.text.Spanned import android.text.TextPaint +import android.text.style.AbsoluteSizeSpan import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan -import android.text.style.RelativeSizeSpan import android.view.View import dagger.Lazy import im.vector.matrix.android.api.permalinks.MatrixLinkify @@ -39,6 +39,8 @@ import im.vector.riotx.core.linkify.VectorLinkify import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.core.utils.DimensionConverter +import im.vector.riotx.core.utils.containsOnlyEmojis import im.vector.riotx.core.utils.isLocalFile import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.* @@ -51,6 +53,7 @@ import javax.inject.Inject class MessageItemFactory @Inject constructor( private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val htmlRenderer: Lazy, private val stringProvider: StringProvider, @@ -247,6 +250,7 @@ class MessageItemFactory @Inject constructor( message(linkifiedBody) } } + .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .searchForPills(isFormatted) .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) @@ -271,7 +275,13 @@ class MessageItemFactory @Inject constructor( editEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) - spannable.setSpan(RelativeSizeSpan(.9f), editStart, editEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + // Note: text size is set to 14sp + spannable.setSpan( + AbsoluteSizeSpan(dimensionConverter.spToPx(13)), + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + spannable.setSpan(object : ClickableSpan() { override fun onClick(widget: View?) { callback?.onEditedDecorationClicked(informationData) @@ -351,4 +361,8 @@ class MessageItemFactory @Inject constructor( VectorLinkify.addLinks(spannable, true) return spannable } + + companion object { + private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5 + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt index e1d03d93fc..39392324aa 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ReactionInfoSimpleItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.reactions import android.widget.TextView import androidx.core.view.isVisible diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt similarity index 65% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt index b4eba4bbec..d5df8f7b40 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt @@ -14,37 +14,38 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.reactions import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout -import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife -import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* +import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.* import javax.inject.Inject /** * Bottom sheet displaying list of reactions for a given event ordered by timestamp */ -class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() { +class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() { - private val viewModel: ViewReactionViewModel by fragmentViewModel(ViewReactionViewModel::class) + private val viewModel: ViewReactionsViewModel by fragmentViewModel(ViewReactionsViewModel::class) - @Inject lateinit var viewReactionViewModelFactory: ViewReactionViewModel.Factory + @Inject lateinit var viewReactionsViewModelFactory: ViewReactionsViewModel.Factory - @BindView(R.id.bottom_sheet_display_reactions_list) - lateinit var epoxyRecyclerView: EpoxyRecyclerView + @BindView(R.id.bottomSheetRecyclerView) + lateinit var recyclerView: RecyclerView @Inject lateinit var epoxyController: ViewReactionsEpoxyController @@ -53,26 +54,25 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false) + val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false) ButterKnife.bind(this, view) return view } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - epoxyRecyclerView.setController(epoxyController) - val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, - LinearLayout.VERTICAL) - epoxyRecyclerView.addItemDecoration(dividerItemDecoration) + recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + recyclerView.adapter = epoxyController.adapter bottomSheetTitle.text = context?.getString(R.string.reactions) } override fun invalidate() = withState(viewModel) { epoxyController.setData(it) + super.invalidate() } companion object { - fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionBottomSheet { + fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionsBottomSheet { val args = Bundle() val parcelableArgs = TimelineEventFragmentArgs( informationData.eventId, @@ -80,7 +80,7 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() { informationData ) args.putParcelable(MvRx.KEY_ARG, parcelableArgs) - return ViewReactionBottomSheet().apply { arguments = args } + return ViewReactionsBottomSheet().apply { arguments = args } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt index 6b1c099261..7fd2edcbfe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsEpoxyController.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.reactions import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt similarity index 83% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt index a3611edd87..208e126022 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.features.home.room.detail.timeline.reactions import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext @@ -30,6 +30,7 @@ import im.vector.matrix.rx.RxRoom import im.vector.matrix.rx.unwrap import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import io.reactivex.Observable import io.reactivex.Single @@ -53,10 +54,10 @@ data class ReactionInfo( /** * Used to display the list of members that reacted to a given event */ -class ViewReactionViewModel @AssistedInject constructor(@Assisted +class ViewReactionsViewModel @AssistedInject constructor(@Assisted initialState: DisplayReactionsViewState, - private val session: Session, - private val dateFormatter: VectorDateFormatter + private val session: Session, + private val dateFormatter: VectorDateFormatter ) : VectorViewModel(initialState) { private val roomId = initialState.roomId @@ -66,14 +67,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted @AssistedInject.Factory interface Factory { - fun create(initialState: DisplayReactionsViewState): ViewReactionViewModel + fun create(initialState: DisplayReactionsViewState): ViewReactionsViewModel } - companion object : MvRxViewModelFactory { + companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? { - val fragment: ViewReactionBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewReactionViewModelFactory.create(state) + override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionsViewModel? { + val fragment: ViewReactionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewReactionsViewModelFactory.create(state) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt index 7302d9d2b8..8271086421 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt @@ -19,14 +19,10 @@ package im.vector.riotx.features.home.room.list import im.vector.matrix.android.api.session.room.model.RoomSummary sealed class RoomListActions { - data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions() - data class ToggleCategory(val category: RoomCategory) : RoomListActions() - data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListActions() - data class RejectInvitation(val roomSummary: RoomSummary) : RoomListActions() - data class FilterWith(val filter: String) : RoomListActions() + object MarkAllRoomsRead : RoomListActions() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index d0957f752b..6665500676 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -18,6 +18,8 @@ package im.vector.riotx.features.home.room.list import android.os.Bundle import android.os.Parcelable +import android.view.Menu +import android.view.MenuItem import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.view.isVisible @@ -78,6 +80,26 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O injector.inject(this) } + private var hasUnreadRooms = false + + override fun getMenuRes() = R.menu.room_list + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_home_mark_all_as_read -> { + roomListViewModel.accept(RoomListActions.MarkAllRoomsRead) + return true + } + } + + return super.onOptionsItemSelected(item) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms + super.onPrepareOptionsMenu(menu) + } + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setupCreateRoomButton() @@ -180,6 +202,20 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O is Fail -> renderFailure(state.asyncFilteredRooms.error) } roomController.update(state) + + // Mark all as read menu + when (roomListParams.displayMode) { + DisplayMode.HOME, + DisplayMode.PEOPLE, + DisplayMode.ROOMS -> { + val newValue = state.hasUnread + if (hasUnreadRooms != newValue) { + hasUnreadRooms = newValue + requireActivity().invalidateOptionsMenu() + } + } + else -> Unit + } } private fun renderSuccess(state: RoomListViewState) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index fcdb2f3138..cb74b1144d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -78,6 +78,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room is RoomListActions.AcceptInvitation -> handleAcceptInvitation(action) is RoomListActions.RejectInvitation -> handleRejectInvitation(action) is RoomListActions.FilterWith -> handleFilter(action) + is RoomListActions.MarkAllRoomsRead -> handleMarkAllRoomsRead() } } @@ -193,6 +194,15 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room }) } + private fun handleMarkAllRoomsRead() = withState { state -> + state.asyncFilteredRooms.invoke() + ?.flatMap { it.value } + ?.filter { it.membership == Membership.JOIN } + ?.map { it.roomId } + ?.toList() + ?.let { session.markAllAsRead(it, object : MatrixCallback {}) } + } + private fun buildRoomSummaries(rooms: List): RoomSummaries { val invites = ArrayList() val favourites = ArrayList() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt index 2f388b60d8..505554a8fb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt @@ -20,6 +20,7 @@ import androidx.annotation.StringRes import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.R @@ -67,6 +68,13 @@ data class RoomListViewState( RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded) } } + + val hasUnread: Boolean + get() = asyncFilteredRooms.invoke() + ?.flatMap { it.value } + ?.filter { it.membership == Membership.JOIN } + ?.any { it.hasUnreadMessages } + ?: false } typealias RoomSummaries = LinkedHashMap> diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index 8929b94771..f9601265d3 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -212,8 +212,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Clear the preferences. - * - * @param context the context */ fun clearPreferences() { val keysToKeep = HashSet(mKeysToKeepAfterLogout) @@ -263,7 +261,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if we have already asked the user to disable battery optimisations on android >= M devices. * - * @param context the context * @return true if it was already requested */ fun didAskUserToIgnoreBatteryOptimizations(): Boolean { @@ -272,8 +269,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Mark as requested the question to disable battery optimisations. - * - * @param context the context */ fun setDidAskUserToIgnoreBatteryOptimizations() { defaultPrefs.edit { @@ -294,7 +289,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the timestamp must be displayed in 12h format * - * @param context the context * @return true if the time must be displayed in 12h format */ fun displayTimeIn12hFormat(): Boolean { @@ -304,7 +298,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the join and leave membership events should be shown in the messages list. * - * @param context the context * @return true if the join and leave membership events should be shown in the messages list */ fun showJoinLeaveMessages(): Boolean { @@ -314,7 +307,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the avatar and display name events should be shown in the messages list. * - * @param context the context * @return true true if the avatar and display name events should be shown in the messages list. */ fun showAvatarDisplayNameChangeMessages(): Boolean { @@ -324,7 +316,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells the native camera to take a photo or record a video. * - * @param context the context * @return true to use the native camera app to record video or take photo. */ fun useNativeCamera(): Boolean { @@ -334,7 +325,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the send voice feature is enabled. * - * @param context the context * @return true if the send voice feature is enabled. */ fun isSendVoiceFeatureEnabled(): Boolean { @@ -344,7 +334,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells which compression level to use by default * - * @param context the context * @return the selected compression level */ fun getSelectedDefaultMediaCompressionLevel(): Int { @@ -354,7 +343,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells which media source to use by default * - * @param context the context * @return the selected media source */ fun getSelectedDefaultMediaSource(): Int { @@ -364,7 +352,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells whether to use shutter sound. * - * @param context the context * @return true if shutter sound should play */ fun useShutterSound(): Boolean { @@ -374,7 +361,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Update the notification ringtone * - * @param context the context * @param uri the new notification ringtone, or null for no RingTone */ fun setNotificationRingTone(uri: Uri?) { @@ -399,7 +385,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provides the selected notification ring tone * - * @param context the context * @return the selected ring tone or null for no RingTone */ fun getNotificationRingTone(): Uri? { @@ -432,7 +417,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provide the notification ringtone filename * - * @param context the context * @return the filename or null if "None" is selected */ fun getNotificationRingToneName(): String? { @@ -455,7 +439,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Enable or disable the lazy loading * - * @param context the context * @param newValue true to enable lazy loading, false to disable it */ fun setUseLazyLoading(newValue: Boolean) { @@ -467,7 +450,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the lazy loading is enabled * - * @param context the context * @return true if the lazy loading of room members is enabled */ fun useLazyLoading(): Boolean { @@ -477,7 +459,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * User explicitly refuses the lazy loading. * - * @param context the context */ fun setUserRefuseLazyLoading() { defaultPrefs.edit { @@ -488,7 +469,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the user has explicitly refused the lazy loading * - * @param context the context * @return true if the user has explicitly refuse the lazy loading of room members */ fun hasUserRefusedLazyLoading(): Boolean { @@ -498,7 +478,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the data save mode is enabled * - * @param context the context * @return true if the data save mode is enabled */ fun useDataSaveMode(): Boolean { @@ -508,7 +487,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the conf calls must be done with Jitsi. * - * @param context the context * @return true if the conference call must be done with jitsi. */ fun useJitsiConfCall(): Boolean { @@ -518,7 +496,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the application is started on boot * - * @param context the context * @return true if the application must be started on boot */ fun autoStartOnBoot(): Boolean { @@ -528,7 +505,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the application is started on boot * - * @param context the context * @param value true to start the application on boot */ fun setAutoStartOnBoot(value: Boolean) { @@ -540,7 +516,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provides the selected saving period. * - * @param context the context * @return the selected period */ fun getSelectedMediasSavingPeriod(): Int { @@ -550,7 +525,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Updates the selected saving period. * - * @param context the context * @param index the selected period index */ fun setSelectedMediasSavingPeriod(index: Int) { @@ -562,7 +536,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provides the minimum last access time to keep a media file. * - * @param context the context * @return the min last access time (in seconds) */ fun getMinMediasLastAccessTime(): Long { @@ -578,7 +551,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Provides the selected saving period. * - * @param context the context * @return the selected period */ fun getSelectedMediasSavingPeriodString(): String { @@ -601,7 +573,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the markdown is enabled * - * @param context the context * @return true if the markdown is enabled */ fun isMarkdownEnabled(): Boolean { @@ -611,7 +582,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Update the markdown enable status. * - * @param context the context * @param isEnabled true to enable the markdown */ fun setMarkdownEnabled(isEnabled: Boolean) { @@ -623,7 +593,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the read receipts should be shown * - * @param context the context * @return true if the read receipts should be shown */ fun showReadReceipts(): Boolean { @@ -633,7 +602,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the message timestamps must be always shown * - * @param context the context * @return true if the message timestamps must be always shown */ fun alwaysShowTimeStamps(): Boolean { @@ -643,7 +611,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the typing notifications should be sent * - * @param context the context * @return true to send the typing notifs */ fun sendTypingNotifs(): Boolean { @@ -653,7 +620,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells of the missing notifications rooms must be displayed at left (home screen) * - * @param context the context * @return true to move the missed notifications to the left side */ fun pinMissedNotifications(): Boolean { @@ -663,7 +629,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells of the unread rooms must be displayed at left (home screen) * - * @param context the context * @return true to move the unread room to the left side */ fun pinUnreadMessages(): Boolean { @@ -673,7 +638,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the phone must vibrate when mentioning * - * @param context the context * @return true */ fun vibrateWhenMentioning(): Boolean { @@ -683,7 +647,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if a dialog has been displayed to ask to use the analytics tracking (piwik, matomo, etc.). * - * @param context the context * @return true if a dialog has been displayed to ask to use the analytics tracking */ fun didAskToUseAnalytics(): Boolean { @@ -693,7 +656,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * To call if the user has been asked for analytics tracking. * - * @param context the context */ fun setDidAskToUseAnalytics() { defaultPrefs.edit { @@ -704,7 +666,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the analytics tracking is authorized (piwik, matomo, etc.). * - * @param context the context * @return true if the analytics tracking is authorized */ fun useAnalytics(): Boolean { @@ -714,7 +675,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Enable or disable the analytics tracking. * - * @param context the context * @param useAnalytics true to enable the analytics tracking */ fun setUseAnalytics(useAnalytics: Boolean) { @@ -726,7 +686,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if media should be previewed before sending * - * @param context the context * @return true to preview media */ fun previewMediaWhenSending(): Boolean { @@ -736,7 +695,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if message should be send by pressing enter on the soft keyboard * - * @param context the context * @return true to send message with enter */ fun sendMessageWithEnter(): Boolean { @@ -746,7 +704,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if the rage shake is used. * - * @param context the context * @return true if the rage shake is used */ fun useRageshake(): Boolean { @@ -756,7 +713,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Update the rage shake status. * - * @param context the context * @param isEnabled true to enable the rage shake */ fun setUseRageshake(isEnabled: Boolean) { @@ -768,7 +724,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { /** * Tells if all the events must be displayed ie even the redacted events. * - * @param context the context * @return true to display all the events even the redacted ones. */ fun displayAllEvents(): Boolean { diff --git a/vector/src/main/res/drawable/ic_report_custom.xml b/vector/src/main/res/drawable/ic_report_custom.xml new file mode 100644 index 0000000000..8e97c4bfb5 --- /dev/null +++ b/vector/src/main/res/drawable/ic_report_custom.xml @@ -0,0 +1,8 @@ + + + diff --git a/vector/src/main/res/drawable/ic_report_inappropriate.xml b/vector/src/main/res/drawable/ic_report_inappropriate.xml new file mode 100644 index 0000000000..47cc0591bd --- /dev/null +++ b/vector/src/main/res/drawable/ic_report_inappropriate.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_report_spam.xml b/vector/src/main/res/drawable/ic_report_spam.xml new file mode 100644 index 0000000000..bd5a46e00a --- /dev/null +++ b/vector/src/main/res/drawable/ic_report_spam.xml @@ -0,0 +1,8 @@ + + + diff --git a/vector/src/main/res/layout/bottom_sheet_generic_list.xml b/vector/src/main/res/layout/bottom_sheet_generic_list.xml new file mode 100644 index 0000000000..69b5ce2fac --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_generic_list.xml @@ -0,0 +1,11 @@ + + + diff --git a/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml b/vector/src/main/res/layout/bottom_sheet_generic_list_with_title.xml similarity index 65% rename from vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml rename to vector/src/main/res/layout/bottom_sheet_generic_list_with_title.xml index 9b3ffb26a3..80d877ac2d 100644 --- a/vector/src/main/res/layout/bottom_sheet_epoxylist_with_title.xml +++ b/vector/src/main/res/layout/bottom_sheet_generic_list_with_title.xml @@ -3,29 +3,25 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:minHeight="400dp" android:orientation="vertical"> - + tools:listitem="@layout/item_simple_reaction_info" /> - \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_message_actions.xml b/vector/src/main/res/layout/bottom_sheet_message_actions.xml deleted file mode 100644 index c7d4f5ac8e..0000000000 --- a/vector/src/main/res/layout/bottom_sheet_message_actions.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/dialog_report_content.xml b/vector/src/main/res/layout/dialog_report_content.xml new file mode 100644 index 0000000000..dda84fe02d --- /dev/null +++ b/vector/src/main/res/layout/dialog_report_content.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_message_menu.xml b/vector/src/main/res/layout/fragment_message_menu.xml deleted file mode 100644 index 4538ac935c..0000000000 --- a/vector/src/main/res/layout/fragment_message_menu.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/adapter_item_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml similarity index 64% rename from vector/src/main/res/layout/adapter_item_action.xml rename to vector/src/main/res/layout/item_bottom_sheet_action.xml index 03d2f81115..131ee0e63c 100644 --- a/vector/src/main/res/layout/adapter_item_action.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml @@ -2,23 +2,29 @@ + android:paddingBottom="8dp"> + + + + diff --git a/vector/src/main/res/layout/item_bottom_sheet_divider.xml b/vector/src/main/res/layout/item_bottom_sheet_divider.xml new file mode 100644 index 0000000000..d130465e7d --- /dev/null +++ b/vector/src/main/res/layout/item_bottom_sheet_divider.xml @@ -0,0 +1,6 @@ + + diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml new file mode 100644 index 0000000000..a688f38d0f --- /dev/null +++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_status.xml b/vector/src/main/res/layout/item_bottom_sheet_message_status.xml new file mode 100644 index 0000000000..10c129cf58 --- /dev/null +++ b/vector/src/main/res/layout/item_bottom_sheet_message_status.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml b/vector/src/main/res/layout/item_bottom_sheet_quick_reaction.xml similarity index 100% rename from vector/src/main/res/layout/adapter_item_action_quick_reaction.xml rename to vector/src/main/res/layout/item_bottom_sheet_quick_reaction.xml diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index ec5cac245d..fbe3b70551 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -117,7 +117,16 @@ android:layout_marginBottom="4dp" app:dividerDrawable="@drawable/reaction_divider" app:flexWrap="wrap" - app:showDivider="middle" /> + app:showDivider="middle" + tools:background="#F0E0F0" + tools:layout_height="40dp"> + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/vector_home.xml b/vector/src/main/res/menu/vector_home.xml deleted file mode 100755 index fe24e6fdf5..0000000000 --- a/vector/src/main/res/menu/vector_home.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index a66858ae0e..30fae49f13 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -37,4 +37,19 @@ "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s." + + "It's spam" + "It's inappropriate" + "Custom report" + "Report this content" + "Reason for reporting this content" + "REPORT" + "BLOCK USER" + + "Content reported" + "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" + "Reported as spam" + "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" + "Reported as inappropriate" + "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"