diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 8fcb69b6c0a..e60595e8b90 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,6 +1,7 @@ # Use Docker file from https://hub.docker.com/r/runmymind/docker-android-sdk # Last docker plugin version can be found here: # https://github.com/buildkite-plugins/docker-buildkite-plugin/releases +# We propagate the environment to the container (sse https://github.com/buildkite-plugins/docker-buildkite-plugin#propagate-environment-optional-boolean) # Build debug version of the RiotX application, from the develop branch and the features branches @@ -18,6 +19,7 @@ steps: plugins: - docker#v3.1.0: image: "runmymind/docker-android-sdk" + propagate-environment: true - label: "Assemble FDroid Debug version" agents: @@ -32,6 +34,7 @@ steps: plugins: - docker#v3.1.0: image: "runmymind/docker-android-sdk" + propagate-environment: true - label: "Build Google Play unsigned APK" agents: @@ -46,6 +49,7 @@ steps: plugins: - docker#v3.1.0: image: "runmymind/docker-android-sdk" + propagate-environment: true # Code quality diff --git a/CHANGES.md b/CHANGES.md index 8dd858783cb..8c2372a3b1a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,33 @@ +Changes in RiotX 0.6.0 (2019-09-24) +=================================================== + +Features: + - Save draft of a message when exiting a room with non empty composer (#329) + +Improvements: + - Add unread indent on room list (#485) + - Message Editing: Update notifications (#128) + - Remove any notification of a redacted event (#563) + +Other changes: + - Fix a few accessibility issues + +Bugfix: + - Fix characters erased from the Search field when the result are coming (#545) + - "No connection" banner was displayed by mistake + - Leaving community (from another client) has no effect on RiotX (#497) + - Push rules was not retrieved after a clear cache + - m.notice messages trigger push notifications (#238) + - Embiggen messages with multiple emojis also for edited messages (#458) + +Build: + - Fix (again) issue with bad versionCode generated by Buildkite (#553) + Changes in RiotX 0.5.0 (2019-09-17) =================================================== Features: + - Implementation of login to homeserver with SSO (#557) - Handle M_CONSENT_NOT_GIVEN error (#64) - Auto configure homeserver and identity server URLs of LoginActivity with a magic link diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 0ff0987dfee..28a3d40070b 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import io.reactivex.Observable import io.reactivex.Single @@ -54,6 +55,10 @@ class RxRoom(private val room: Room) { return room.getEventReadReceiptsLive(eventId).asObservable() } + fun liveDrafts(): Observable> { + return room.getDraftsLive().asObservable() + } + } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt index 152adb06653..26170a288ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt @@ -31,6 +31,7 @@ import java.io.IOException */ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { data class Unknown(val throwable: Throwable? = null) : Failure(throwable) + data class Cancelled(val throwable: Throwable? = null) : Failure(throwable) data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException) data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) // When server send an error, but it cannot be interpreted as a MatrixError diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/Action.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/Action.kt index 797830f4916..b34d2d0d341 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/Action.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/Action.kt @@ -19,77 +19,92 @@ import im.vector.matrix.android.api.pushrules.rest.PushRule import timber.log.Timber -class Action(val type: Type) { +sealed class Action { + object Notify : Action() + object DoNotNotify : Action() + data class Sound(val sound: String) : Action() + data class Highlight(val highlight: Boolean) : Action() +} - enum class Type(val value: String) { - NOTIFY("notify"), - DONT_NOTIFY("dont_notify"), - COALESCE("coalesce"), - SET_TWEAK("set_tweak"); - companion object { +private const val ACTION_NOTIFY = "notify" +private const val ACTION_DONT_NOTIFY = "dont_notify" +private const val ACTION_COALESCE = "coalesce" - fun safeValueOf(value: String): Type? { - try { - return valueOf(value) - } catch (e: IllegalArgumentException) { - return null - } - } - } - } +// Ref: https://matrix.org/docs/spec/client_server/latest#tweaks +private const val ACTION_OBJECT_SET_TWEAK_KEY = "set_tweak" - var tweak_action: String? = null - var stringValue: String? = null - var boolValue: Boolean? = null +private const val ACTION_OBJECT_SET_TWEAK_VALUE_SOUND = "sound" +private const val ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT = "highlight" + +private const val ACTION_OBJECT_VALUE_KEY = "value" +private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default" + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#actions + * + * Convert + *
+ * "actions": [
+ *     "notify",
+ *     {
+ *         "set_tweak": "sound",
+ *         "value": "default"
+ *     },
+ *     {
+ *         "set_tweak": "highlight"
+ *     }
+ *   ]
+ *
+ * To
+ * [
+ *     Action.Notify,
+ *     Action.Sound("default"),
+ *     Action.Highlight(true)
+ * ]
+ *
+ * 
+ */ +fun PushRule.getActions(): List { + val result = ArrayList() - companion object { - fun mapFrom(pushRule: PushRule): List? { - val actions = ArrayList() - pushRule.actions.forEach { actionStrOrObj -> - if (actionStrOrObj is String) { - when (actionStrOrObj) { - Action.Type.NOTIFY.value -> Action(Action.Type.NOTIFY) - Action.Type.DONT_NOTIFY.value -> Action(Action.Type.DONT_NOTIFY) - else -> { - Timber.w("Unsupported action type ${actionStrOrObj}") - null + actions.forEach { actionStrOrObj -> + when (actionStrOrObj) { + ACTION_NOTIFY -> Action.Notify + ACTION_DONT_NOTIFY -> Action.DoNotNotify + is Map<*, *> -> { + when (actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]) { + ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> { + (actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? String)?.let { stringValue -> + Action.Sound(stringValue) } - }?.let { - actions.add(it) + // When the value is not there, default sound (not specified by the spec) + ?: Action.Sound(ACTION_OBJECT_VALUE_VALUE_DEFAULT) + } - } else if (actionStrOrObj is Map<*, *>) { - val tweakAction = actionStrOrObj["set_tweak"] as? String - when (tweakAction) { - "sound" -> { - (actionStrOrObj["value"] as? String)?.let { stringValue -> - Action(Action.Type.SET_TWEAK).also { - it.tweak_action = "sound" - it.stringValue = stringValue - actions.add(it) - } - } - } - "highlight" -> { - (actionStrOrObj["value"] as? Boolean)?.let { boolValue -> - Action(Action.Type.SET_TWEAK).also { - it.tweak_action = "highlight" - it.boolValue = boolValue - actions.add(it) - } - } - } - else -> { - Timber.w("Unsupported action type ${actionStrOrObj}") + ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT -> { + (actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? Boolean)?.let { boolValue -> + Action.Highlight(boolValue) } + // When the value is not there, default is true, says the spec + ?: Action.Highlight(true) + } + else -> { + Timber.w("Unsupported set_tweak value ${actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]}") + null } - } else { - Timber.w("Unsupported action type ${actionStrOrObj}") - return null } } - return if (actions.isEmpty()) null else actions + else -> { + Timber.w("Unsupported action type $actionStrOrObj") + null + } + }?.let { + result.add(it) } } + + + return result } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt index 28fcff0c762..c0204d181d7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/PushRuleService.kt @@ -25,14 +25,14 @@ interface PushRuleService { /** * Fetch the push rules from the server */ - fun fetchPushRules(scope: String = "global") + fun fetchPushRules(scope: String = RuleScope.GLOBAL) //TODO get push rule set - fun getPushRules(scope: String = "global"): List + fun getPushRules(scope: String = RuleScope.GLOBAL): List //TODO update rule - fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable + fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable fun addPushRuleListener(listener: PushRuleListener) @@ -43,6 +43,7 @@ interface PushRuleService { interface PushRuleListener { fun onMatchRule(event: Event, actions: List) fun onRoomLeft(roomId: String) + fun onEventRedacted(redactedEventId: String) fun batchFinish() } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RuleIds.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RuleIds.kt index 38a64adf27d..cf05e9252d4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RuleIds.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RuleIds.kt @@ -37,7 +37,7 @@ object RuleIds { // Default Underride Rules const val RULE_ID_CALL = ".m.rule.call" - const val RULE_ID_one_to_one_encrypted_room = ".m.rule.encrypted_room_one_to_one" + const val RULE_ID_ONE_TO_ONE_ENCRYPTED_ROOM = ".m.rule.encrypted_room_one_to_one" const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one" const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message" const val RULE_ID_ENCRYPTED = ".m.rule.encrypted" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RulesetKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RuleScope.kt similarity index 78% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RulesetKey.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RuleScope.kt index 834bdf2add0..19aa2a4d43b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RulesetKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RuleScope.kt @@ -15,12 +15,6 @@ */ package im.vector.matrix.android.api.pushrules - -enum class RulesetKey(val value: String) { - CONTENT("content"), - OVERRIDE("override"), - ROOM("room"), - SENDER("sender"), - UNDERRIDE("underride"), - UNKNOWN("") -} \ No newline at end of file +object RuleScope { + const val GLOBAL = "global" +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RuleSetKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RuleSetKey.kt new file mode 100644 index 00000000000..e8862ad343a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/RuleSetKey.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.api.pushrules + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules + */ +enum class RuleSetKey(val value: String) { + CONTENT("content"), + OVERRIDE("override"), + ROOM("room"), + SENDER("sender"), + UNDERRIDE("underride") +} + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules-scope-kind-ruleid + */ +typealias RuleKind = RuleSetKey diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushCondition.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushCondition.kt index 8f694016534..7b71a344a01 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushCondition.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/rest/PushCondition.kt @@ -71,7 +71,7 @@ data class PushCondition( this.key?.let { SenderNotificationPermissionCondition(it) } } Condition.Kind.UNRECOGNIZE -> { - Timber.e("Unknwon kind $kind") + Timber.e("Unknown kind $kind") null } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/model/GroupSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/model/GroupSummary.kt index 3089d83dde0..328ca746a64 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/model/GroupSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/model/GroupSummary.kt @@ -16,12 +16,15 @@ package im.vector.matrix.android.api.session.group.model +import im.vector.matrix.android.api.session.room.model.Membership + /** * This class holds some data of a group. * It can be retrieved through [im.vector.matrix.android.api.session.group.GroupService] */ data class GroupSummary( val groupId: String, + val membership: Membership, val displayName: String = "", val shortDescription: String = "", val avatarUrl: String = "", diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/Pusher.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/Pusher.kt index 7b1fa3bb526..baedce96932 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/Pusher.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/Pusher.kt @@ -16,9 +16,6 @@ package im.vector.matrix.android.api.session.pushers data class Pusher( - - val userId: String, - val pushKey: String, val kind: String, val appId: String, 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 ec6b382f8fa..92414eb768d 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 @@ -22,6 +22,7 @@ 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.read.ReadService +import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService @@ -32,6 +33,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService interface Room : TimelineService, SendService, + DraftService, ReadService, MembershipService, StateService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index aae72dd41f6..099deae9372 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.model import im.vector.matrix.android.api.session.room.model.tag.RoomTag +import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent /** @@ -29,13 +30,15 @@ data class RoomSummary( val topic: String = "", val avatarUrl: String = "", val isDirect: Boolean = false, - val latestEvent: TimelineEvent? = null, + val latestPreviewableEvent: TimelineEvent? = null, val otherMemberIds: List = emptyList(), val notificationCount: Int = 0, val highlightCount: Int = 0, + val hasUnreadMessages: Boolean = false, val tags: List = emptyList(), val membership: Membership = Membership.NONE, - val versioningState: VersioningState = VersioningState.NONE + val versioningState: VersioningState = VersioningState.NONE, + val userDrafts: List = emptyList() ) { val isVersioned: Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index cdc1c9527ef..c707f3c3efa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -20,7 +20,6 @@ import android.util.Patterns import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.MatrixPatterns.isUserId -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType @@ -219,7 +218,7 @@ class CreateRoomParams { * @param ids the participant ids to add. */ fun addParticipantIds(hsConfig: HomeServerConnectionConfig, - credentials: Credentials, + userId: String, ids: List) { for (id in ids) { if (Patterns.EMAIL_ADDRESS.matcher(id).matches() && hsConfig.identityServerUri != null) { @@ -233,7 +232,7 @@ class CreateRoomParams { invite3pids!!.add(pid) } else if (isUserId(id)) { // do not invite oneself - if (credentials.userId != id) { + if (userId != id) { if (null == invitedUserIds) { invitedUserIds = ArrayList() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt new file mode 100644 index 00000000000..c700b40a088 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.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.api.session.room.send + +import androidx.lifecycle.LiveData + +interface DraftService { + + /** + * Save or update a draft to the room + */ + fun saveDraft(draft: UserDraft) + + /** + * Delete the last draft, basically just after sending the message + */ + fun deleteDraft() + + /** + * Return the current drafts if any, as a live data + * The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts + */ + fun getDraftsLive(): LiveData> + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt new file mode 100644 index 00000000000..8912cc2580b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt @@ -0,0 +1,38 @@ +/* + * 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.send + +/** + * Describes a user draft: + * REGULAR: draft of a classical message + * QUOTE: draft of a message which quotes another message + * EDIT: draft of an edition of a message + * REPLY: draft of a reply of another message + */ +sealed class UserDraft(open val text: String) { + data class REGULAR(override val text: String) : UserDraft(text) + data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text) + data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text) + data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text) + + fun isValid(): Boolean { + return when (this) { + is REGULAR -> text.isNotBlank() + else -> true + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 36ca360e088..73ef7b779a9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.timeline import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReadReceipt @@ -92,6 +93,13 @@ data class TimelineEvent( */ fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null +/** + * Get the eventId which was edited by this event if any + */ +fun TimelineEvent.getEditedEventId(): String? { + return root.getClearContent().toModel()?.relatesTo?.takeIf { it.type == RelationType.REPLACE }?.eventId +} + /** * Get last MessageContent, after a possible edition */ @@ -99,6 +107,20 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSu ?: root.getClearContent().toModel() +/** + * Get last Message body, after a possible edition + */ +fun TimelineEvent.getLastMessageBody(): String? { + val lastMessageContent = getLastMessageContent() + + if (lastMessageContent != null) { + return lastMessageContent.newContent?.toModel()?.body ?: lastMessageContent.body + } + + return null +} + + fun TimelineEvent.getTextEditableContent(): String? { val originalContent = root.getClearContent().toModel() ?: return null val isReply = originalContent.isReply() || root.content.toModel()?.relatesTo?.inReplyTo?.eventId != null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt index 38d3e695896..625f81de86b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ObjectSigner.kt @@ -20,7 +20,7 @@ import im.vector.matrix.android.api.auth.data.Credentials import javax.inject.Inject internal class ObjectSigner @Inject constructor(private val credentials: Credentials, - private val olmDevice: MXOlmDevice) { + private val olmDevice: MXOlmDevice) { /** * Sign Object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt index 840a66c5430..f929859d765 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt @@ -27,14 +27,14 @@ import java.util.* import javax.inject.Inject internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice, - private val cryptoStore: IMXCryptoStore, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) { + private val cryptoStore: IMXCryptoStore, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) { /** * Try to make sure we have established olm sessions for the given users. * @param users a list of user ids. */ - suspend fun handle(users: List) : MXUsersDevicesMap { + suspend fun handle(users: List): MXUsersDevicesMap { Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") val devicesByUser = HashMap>() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt index 7fc39312e19..078d016e429 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt @@ -16,15 +16,15 @@ package im.vector.matrix.android.internal.crypto.actions -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.di.UserId import timber.log.Timber import javax.inject.Inject internal class SetDeviceVerificationAction @Inject constructor(private val cryptoStore: IMXCryptoStore, - private val credentials: Credentials, - private val keysBackup: KeysBackup) { + @UserId private val userId: String, + private val keysBackup: KeysBackup) { fun handle(verificationStatus: Int, deviceId: String, userId: String) { val device = cryptoStore.getUserDevice(deviceId, userId) @@ -39,7 +39,7 @@ internal class SetDeviceVerificationAction @Inject constructor(private val crypt device.verified = verificationStatus cryptoStore.storeUserDevice(userId, device) - if (userId == credentials.userId) { + if (userId == this.userId) { // If one of the user's own devices is being marked as verified / unverified, // check the key backup status, since whether or not we use this depends on // whether it has a signature from a verified device diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 47bf9956b97..c518872d448 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm import android.text.TextUtils -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType @@ -40,7 +39,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber -internal class MXMegolmDecryption(private val credentials: Credentials, +internal class MXMegolmDecryption(private val userId: String, private val olmDevice: MXOlmDevice, private val deviceListManager: DeviceListManager, private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, @@ -146,11 +145,11 @@ internal class MXMegolmDecryption(private val credentials: Credentials, val selfMap = HashMap() // TODO Replace this hard coded keys (see OutgoingRoomKeyRequestManager) - selfMap["userId"] = credentials.userId + selfMap["userId"] = userId selfMap["deviceId"] = "*" recipients.add(selfMap) - if (!TextUtils.equals(sender, credentials.userId)) { + if (!TextUtils.equals(sender, userId)) { val senderMap = HashMap() senderMap["userId"] = sender senderMap["deviceId"] = encryptedEventContent.deviceId!! diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index eb8df7b9f85..54c412526dc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager @@ -24,22 +23,23 @@ import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevi import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject -internal class MXMegolmDecryptionFactory @Inject constructor(private val credentials: Credentials, - private val olmDevice: MXOlmDevice, - private val deviceListManager: DeviceListManager, - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers) { +internal class MXMegolmDecryptionFactory @Inject constructor(@UserId private val userId: String, + private val olmDevice: MXOlmDevice, + private val deviceListManager: DeviceListManager, + private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val cryptoStore: IMXCryptoStore, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers) { fun create(): MXMegolmDecryption { return MXMegolmDecryption( - credentials, + userId, olmDevice, deviceListManager, outgoingRoomKeyRequestManager, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt index ede99e4b944..9433e364cba 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.olm -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel @@ -35,8 +34,8 @@ import timber.log.Timber internal class MXOlmDecryption( // The olm device interface private val olmDevice: MXOlmDevice, - // the matrix credentials - private val credentials: Credentials) + // the matrix userId + private val userId: String) : IMXDecrypting { override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { @@ -97,9 +96,9 @@ internal class MXOlmDecryption( throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason) } - if (olmPayloadContent.recipient != credentials.userId) { + if (olmPayloadContent.recipient != userId) { Timber.e("## decryptEvent() : Event ${event.eventId}:" + - " Intended recipient ${olmPayloadContent.recipient} does not match our id ${credentials.userId}") + " Intended recipient ${olmPayloadContent.recipient} does not match our id $userId") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT, String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt index afe4b36e97f..12937a2b2eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt @@ -16,16 +16,16 @@ package im.vector.matrix.android.internal.crypto.algorithms.olm -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.MXOlmDevice +import im.vector.matrix.android.internal.di.UserId import javax.inject.Inject internal class MXOlmDecryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice, - private val credentials: Credentials) { + @UserId private val userId: String) { fun create(): MXOlmDecryption { return MXOlmDecryption( olmDevice, - credentials) + userId) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt index 060b57baa46..eb23f02275e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -16,11 +16,11 @@ package im.vector.matrix.android.internal.crypto.tasks -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceAuth import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -34,7 +34,7 @@ internal interface DeleteDeviceWithUserPasswordTask : Task UserDraft + */ +internal object DraftMapper { + + fun map(entity: DraftEntity): UserDraft { + return when (entity.draftMode) { + DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content) + DraftEntity.MODE_EDIT -> UserDraft.EDIT(entity.linkedEventId, entity.content) + DraftEntity.MODE_QUOTE -> UserDraft.QUOTE(entity.linkedEventId, entity.content) + DraftEntity.MODE_REPLY -> UserDraft.REPLY(entity.linkedEventId, entity.content) + else -> null + } ?: UserDraft.REGULAR("") + } + + fun map(domain: UserDraft): DraftEntity { + return when (domain) { + is UserDraft.REGULAR -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "") + is UserDraft.EDIT -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId) + is UserDraft.QUOTE -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId) + is UserDraft.REPLY -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId) + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/GroupSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/GroupSummaryMapper.kt index 83252352a86..44cbf2998da 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/GroupSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/GroupSummaryMapper.kt @@ -22,14 +22,15 @@ import im.vector.matrix.android.internal.database.model.GroupSummaryEntity internal object GroupSummaryMapper { - fun map(roomSummaryEntity: GroupSummaryEntity): GroupSummary { + fun map(groupSummaryEntity: GroupSummaryEntity): GroupSummary { return GroupSummary( - roomSummaryEntity.groupId, - roomSummaryEntity.displayName, - roomSummaryEntity.shortDescription, - roomSummaryEntity.avatarUrl, - roomSummaryEntity.roomIds.toList(), - roomSummaryEntity.userIds.toList() + groupSummaryEntity.groupId, + groupSummaryEntity.membership, + groupSummaryEntity.displayName, + groupSummaryEntity.shortDescription, + groupSummaryEntity.avatarUrl, + groupSummaryEntity.roomIds.toList(), + groupSummaryEntity.userIds.toList() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushersMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushersMapper.kt index 8cac50a4680..d11532c5887 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushersMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PushersMapper.kt @@ -26,7 +26,6 @@ internal object PushersMapper { fun map(pushEntity: PusherEntity): Pusher { return Pusher( - userId = pushEntity.userId, pushKey = pushEntity.pushKey, kind = pushEntity.kind ?: "", appId = pushEntity.appId, @@ -39,9 +38,8 @@ internal object PushersMapper { ) } - fun map(pusher: JsonPusher, userId: String): PusherEntity { + fun map(pusher: JsonPusher): PusherEntity { return PusherEntity( - userId = userId, pushKey = pusher.pushKey, kind = pusher.kind, appId = pusher.appId, @@ -58,6 +56,6 @@ internal fun PusherEntity.asDomain(): Pusher { return PushersMapper.map(this) } -internal fun JsonPusher.toEntity(userId: String): PusherEntity { - return PushersMapper.map(this, userId) +internal fun JsonPusher.toEntity(): PusherEntity { + return PushersMapper.map(this) } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 3ec51226aff..4fbe7fe04c4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -22,12 +22,12 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import java.util.UUID +import java.util.* import javax.inject.Inject internal class RoomSummaryMapper @Inject constructor( - val cryptoService: CryptoService, - val timelineEventMapper: TimelineEventMapper + private val cryptoService: CryptoService, + private val timelineEventMapper: TimelineEventMapper ) { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { @@ -35,7 +35,7 @@ internal class RoomSummaryMapper @Inject constructor( RoomTag(it.tagName, it.tagOrder) } - val latestEvent = roomSummaryEntity.latestEvent?.let { + val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let { timelineEventMapper.map(it) } if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) { @@ -43,29 +43,32 @@ internal class RoomSummaryMapper @Inject constructor( //for now decrypt sync try { val result = cryptoService.decryptEvent(latestEvent.root, latestEvent.root.roomId + UUID.randomUUID().toString()) - latestEvent.root.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) + latestEvent.root.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) } catch (e: MXCryptoError) { } } + return RoomSummary( roomId = roomSummaryEntity.roomId, displayName = roomSummaryEntity.displayName ?: "", topic = roomSummaryEntity.topic ?: "", avatarUrl = roomSummaryEntity.avatarUrl ?: "", isDirect = roomSummaryEntity.isDirect, - latestEvent = latestEvent, + latestPreviewableEvent = latestEvent, otherMemberIds = roomSummaryEntity.otherMemberIds.toList(), highlightCount = roomSummaryEntity.highlightCount, notificationCount = roomSummaryEntity.notificationCount, + hasUnreadMessages = roomSummaryEntity.hasUnreadMessages, tags = tags, membership = roomSummaryEntity.membership, - versioningState = roomSummaryEntity.versioningState + versioningState = roomSummaryEntity.versioningState, + userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt new file mode 100644 index 00000000000..9666ebd9a15 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt @@ -0,0 +1,34 @@ +/* + * 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.database.model + +import io.realm.RealmObject + +internal open class DraftEntity(var content: String = "", + var draftMode: String = MODE_REGULAR, + var linkedEventId: String = "" + +) : RealmObject() { + + companion object { + const val MODE_REGULAR = "REGULAR" + const val MODE_EDIT = "EDIT" + const val MODE_REPLY = "REPLY" + const val MODE_QUOTE = "QUOTE" + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt index dea36343d52..433f209501d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt @@ -20,9 +20,13 @@ import im.vector.matrix.android.api.session.room.model.Membership import io.realm.RealmObject import io.realm.annotations.PrimaryKey -internal open class GroupEntity(@PrimaryKey var groupId: String = "" - -) : RealmObject() { +/** + * This class is used to store group info (groupId and membership) from the sync response. + * Then [im.vector.matrix.android.internal.session.group.GroupSummaryUpdater] observes change and + * makes requests to fetch group information from the homeserver + */ +internal open class GroupEntity(@PrimaryKey var groupId: String = "") + : RealmObject() { private var membershipStr: String = Membership.NONE.name var membership: Membership diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupSummaryEntity.kt index 5ed8c7f0598..7ab67b6ab08 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupSummaryEntity.kt @@ -16,18 +16,28 @@ package im.vector.matrix.android.internal.database.model +import im.vector.matrix.android.api.session.room.model.Membership import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey internal open class GroupSummaryEntity(@PrimaryKey var groupId: String = "", - var displayName: String = "", - var shortDescription: String = "", - var avatarUrl: String = "", - var roomIds: RealmList = RealmList(), - var userIds: RealmList = RealmList() + var displayName: String = "", + var shortDescription: String = "", + var avatarUrl: String = "", + var roomIds: RealmList = RealmList(), + var userIds: RealmList = RealmList() ) : RealmObject() { + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + companion object } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRulesEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRulesEntity.kt index f504192550a..e0acfa49ae4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRulesEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRulesEntity.kt @@ -15,17 +15,24 @@ */ package im.vector.matrix.android.internal.database.model +import im.vector.matrix.android.api.pushrules.RuleKind import io.realm.RealmList import io.realm.RealmObject -import io.realm.annotations.Index internal open class PushRulesEntity( - @Index var userId: String = "", var scope: String = "", - // "content", etc. - var rulesetKey: String = "", var pushRules: RealmList = RealmList() ) : RealmObject() { + + private var kindStr: String = RuleKind.CONTENT.name + var kind: RuleKind + get() { + return RuleKind.valueOf(kindStr) + } + set(value) { + kindStr = value.name + } + companion object } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PusherEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PusherEntity.kt index a9d33c86bcd..6ec9d4b0de3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PusherEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PusherEntity.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.database.model import im.vector.matrix.android.api.session.pushers.PusherState import io.realm.RealmObject -import io.realm.annotations.Index //TODO // at java.lang.Thread.run(Thread.java:764) @@ -29,7 +28,6 @@ import io.realm.annotations.Index // at im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker$doWork$$inlined$fold$lambda$2.execute(AddHttpPusherWorker.kt:70) // at io.realm.Realm.executeTransaction(Realm.java:1493) internal open class PusherEntity( - @Index var userId: String = "", var pushKey: String = "", var kind: String? = null, var appId: String = "", diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 6fe81f4cdd7..1c159b23d23 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -26,7 +26,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var displayName: String? = "", var avatarUrl: String? = "", var topic: String? = "", - var latestEvent: TimelineEventEntity? = null, + var latestPreviewableEvent: TimelineEventEntity? = null, var heroes: RealmList = RealmList(), var joinedMembersCount: Int? = 0, var invitedMembersCount: Int? = 0, @@ -35,7 +35,9 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var otherMemberIds: RealmList = RealmList(), var notificationCount: Int = 0, var highlightCount: Int = 0, - var tags: RealmList = RealmList() + var hasUnreadMessages: Boolean = false, + var tags: RealmList = RealmList(), + var userDrafts: UserDraftsEntity? = null ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 1d27bf07ee0..680e2eac7d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -43,6 +43,8 @@ import io.realm.annotations.RealmModule PushConditionEntity::class, PusherEntity::class, PusherDataEntity::class, - ReadReceiptsSummaryEntity::class + ReadReceiptsSummaryEntity::class, + UserDraftsEntity::class, + DraftEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt new file mode 100644 index 00000000000..b713fe1c3f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt @@ -0,0 +1,36 @@ +/* + * 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.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects + +/** + * Create a specific table to be able to do direct query on it and keep the draft ordered + */ +internal open class UserDraftsEntity(var userDrafts: RealmList = RealmList() +) : RealmObject() { + + // Link to RoomSummaryEntity + @LinkingObjects("userDrafts") + val roomSummaryEntity: RealmResults? = null + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt index 33c9d868e12..802bfbeae6b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt @@ -23,9 +23,9 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where -internal fun GroupEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { +internal fun GroupEntity.Companion.where(realm: Realm, groupId: String): RealmQuery { return realm.where() - .equalTo(GroupEntityFields.GROUP_ID, roomId) + .equalTo(GroupEntityFields.GROUP_ID, groupId) } internal fun GroupEntity.Companion.where(realm: Realm, membership: Membership? = null): RealmQuery { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt index d0351eec545..601da098ca2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupSummaryEntityQueries.kt @@ -30,3 +30,7 @@ internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupId: String? = return query } +internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupIds: List): RealmQuery { + return realm.where() + .`in`(GroupSummaryEntityFields.GROUP_ID, groupIds.toTypedArray()) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt index b7f54f17ea3..4ecb40a7e17 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/PushersQueries.kt @@ -15,6 +15,7 @@ */ package im.vector.matrix.android.internal.database.query +import im.vector.matrix.android.api.pushrules.RuleKind import im.vector.matrix.android.internal.database.model.PushRulesEntity import im.vector.matrix.android.internal.database.model.PushRulesEntityFields import im.vector.matrix.android.internal.database.model.PusherEntity @@ -24,10 +25,8 @@ import io.realm.RealmQuery import io.realm.kotlin.where internal fun PusherEntity.Companion.where(realm: Realm, - userId: String, pushKey: String? = null): RealmQuery { return realm.where() - .equalTo(PusherEntityFields.USER_ID, userId) .apply { if (pushKey != null) { equalTo(PusherEntityFields.PUSH_KEY, pushKey) @@ -36,11 +35,9 @@ internal fun PusherEntity.Companion.where(realm: Realm, } internal fun PushRulesEntity.Companion.where(realm: Realm, - userId: String, scope: String, - ruleSetKey: String): RealmQuery { + kind: RuleKind): RealmQuery { return realm.where() - .equalTo(PushRulesEntityFields.USER_ID, userId) .equalTo(PushRulesEntityFields.SCOPE, scope) - .equalTo(PushRulesEntityFields.RULESET_KEY, ruleSetKey) + .equalTo(PushRulesEntityFields.KIND_STR, kind.name) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt new file mode 100644 index 00000000000..bd7e3a7a5e3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadQueries.kt @@ -0,0 +1,48 @@ +/* + * 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.database.query + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ReadReceiptEntity + +internal fun isEventRead(monarchy: Monarchy, + userId: String?, + roomId: String?, + eventId: String?): Boolean { + if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) { + return false + } + + var isEventRead = false + + monarchy.doWithRealm { realm -> + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm + val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root + + isEventRead = if (eventToCheck?.sender == userId) { + true + } else { + val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@doWithRealm + val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex ?: Int.MIN_VALUE + val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE + + eventToCheckIndex <= readReceiptIndex + } + } + + return isEventRead +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.kt new file mode 100644 index 00000000000..ae368c5850b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.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.database.query + +import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields +import im.vector.matrix.android.internal.database.model.UserDraftsEntity +import im.vector.matrix.android.internal.database.model.UserDraftsEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun UserDraftsEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { + val query = realm.where() + if (roomId != null) { + query.equalTo(UserDraftsEntityFields.ROOM_SUMMARY_ENTITY + "." + RoomSummaryEntityFields.ROOM_ID, roomId) + } + return query +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt index e4303298923..81de47948cf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter import im.vector.matrix.android.internal.session.sync.model.UserAccountData import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages import im.vector.matrix.android.internal.session.sync.model.UserAccountDataFallback +import im.vector.matrix.android.internal.session.sync.model.UserAccountDataPushRules object MoshiProvider { @@ -31,6 +32,7 @@ object MoshiProvider { .add(UriMoshiAdapter()) .add(RuntimeJsonAdapterFactory.of(UserAccountData::class.java, "type", UserAccountDataFallback::class.java) .registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES) + .registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES) ) .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt index 8ca81a1dab0..0e386185905 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt @@ -18,6 +18,16 @@ package im.vector.matrix.android.internal.di import javax.inject.Qualifier +/** + * Used to inject the userId + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class UserId + +/** + * Used to inject the md5 of the userId + */ @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class UserMd5 +internal annotation class UserMd5 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt index ed1702ec07e..be012dd308f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.network import android.content.Context import com.novoda.merlin.Merlin +import com.novoda.merlin.MerlinsBeard import im.vector.matrix.android.internal.di.MatrixScope +import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import timber.log.Timber import java.util.* import javax.inject.Inject @@ -26,7 +28,9 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @MatrixScope -internal class NetworkConnectivityChecker @Inject constructor(context: Context) { +internal class NetworkConnectivityChecker @Inject constructor(context: Context, + backgroundDetectionObserver: BackgroundDetectionObserver) + : BackgroundDetectionObserver.Listener { private val merlin = Merlin.Builder() .withConnectableCallbacks() @@ -36,11 +40,16 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context) private val listeners = Collections.synchronizedSet(LinkedHashSet()) // True when internet is available - var hasInternetAccess = false + var hasInternetAccess = MerlinsBeard.Builder().build(context).isConnected private set init { + backgroundDetectionObserver.register(this) + } + + override fun onMoveToForeground() { merlin.bind() + merlin.registerDisconnectable { if (hasInternetAccess) { Timber.v("On Disconnect") @@ -63,6 +72,10 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context) } } + override fun onMoveToBackground() { + merlin.unbind() + } + suspend fun waitUntilConnected() { if (hasInternetAccess) { return diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 4be2d4a27fa..ede9e823bfa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.internal.di.MoshiProvider +import kotlinx.coroutines.CancellationException import okhttp3.ResponseBody import org.greenrobot.eventbus.EventBus import retrofit2.Call @@ -49,6 +50,7 @@ internal class Request { is IOException -> Failure.NetworkConnection(exception) is Failure.ServerError, is Failure.OtherServerError -> exception + is CancellationException -> Failure.Cancelled(exception) else -> Failure.Unknown(exception) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt index 306b8a91e69..5c344a0b11f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UserAgentHolder.kt @@ -24,7 +24,7 @@ import timber.log.Timber import javax.inject.Inject @MatrixScope -internal class UserAgentHolder @Inject constructor(val context: Context) { +internal class UserAgentHolder @Inject constructor(private val context: Context) { var userAgent: String = "" private set diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index 0f1554ea42c..456e7873bb9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -20,11 +20,11 @@ import android.content.Context import android.os.Environment import arrow.core.Try import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments +import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.md5 @@ -40,7 +40,7 @@ import java.io.IOException import javax.inject.Inject internal class DefaultFileService @Inject constructor(private val context: Context, - private val sessionParams: SessionParams, + @UserMd5 private val userMd5: String, private val contentUrlResolver: ContentUrlResolver, private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { @@ -105,7 +105,7 @@ internal class DefaultFileService @Inject constructor(private val context: Conte // Create dir tree (MF stands for Matrix File): // /MF/// val tmpFolderRoot = File(context.cacheDir, "MF") - val tmpFolderUser = File(tmpFolderRoot, sessionParams.credentials.userId.md5()) + val tmpFolderUser = File(tmpFolderRoot, userMd5) File(tmpFolderUser, id.md5()) } FileService.DownloadMode.TO_EXPORT -> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 180cdb6ea25..a08c7e4ab79 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -67,11 +67,18 @@ internal abstract class SessionModule { return sessionParams.credentials } + @JvmStatic + @UserId + @Provides + fun providesUserId(credentials: Credentials): String { + return credentials.userId + } + @JvmStatic @UserMd5 @Provides - fun providesUserMd5(sessionParams: SessionParams): String { - return sessionParams.credentials.userId.md5() + fun providesUserMd5(@UserId userId: String): String { + return userId.md5() } @JvmStatic diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt index a5da026aecd..8df74262632 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.session.filter -import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -33,7 +33,7 @@ internal interface SaveFilterTask : Task { } -internal class DefaultSaveFilterTask @Inject constructor(private val sessionParams: SessionParams, +internal class DefaultSaveFilterTask @Inject constructor(@UserId private val userId: String, private val filterAPI: FilterApi, private val filterRepository: FilterRepository ) : SaveFilterTask { @@ -41,7 +41,7 @@ internal class DefaultSaveFilterTask @Inject constructor(private val sessionPara override suspend fun execute(params: SaveFilterTask.Params) { val filterResponse = executeRequest { // TODO auto retry - apiCall = filterAPI.uploadFilter(sessionParams.credentials.userId, params.filter) + apiCall = filterAPI.uploadFilter(userId, params.filter) } filterRepository.storeFilterId(params.filter, filterResponse.filterId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt index 6964ccf83c6..003f6a8c61d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGetGroupDataTask.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.group import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest @@ -64,8 +65,7 @@ internal class DefaultGetGroupDataTask @Inject constructor( groupSummaryEntity.avatarUrl = groupSummary.profile?.avatarUrl ?: "" val name = groupSummary.profile?.name groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name - groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription - ?: "" + groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: "" val roomIds = groupRooms.rooms.map { it.roomId } groupSummaryEntity.roomIds.clear() @@ -74,8 +74,12 @@ internal class DefaultGetGroupDataTask @Inject constructor( val userIds = groupUsers.users.map { it.userId } groupSummaryEntity.userIds.clear() groupSummaryEntity.userIds.addAll(userIds) + + groupSummaryEntity.membership = when (groupSummary.user?.membership) { + Membership.JOIN.value -> Membership.JOIN + Membership.INVITE.value -> Membership.INVITE + else -> Membership.LEAVE + } } } - - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index 47905ecc370..12a256e7dc0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -20,35 +20,48 @@ import android.content.Context import androidx.work.ExistingWorkPolicy import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.model.GroupEntity +import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory import io.realm.OrderedCollectionChangeSet -import io.realm.RealmConfiguration import io.realm.RealmResults import javax.inject.Inject private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" internal class GroupSummaryUpdater @Inject constructor(private val context: Context, - private val credentials: Credentials, - @SessionDatabase realmConfiguration: RealmConfiguration) - : RealmLiveEntityObserver(realmConfiguration) { + @UserId private val userId: String, + private val monarchy: Monarchy) + : RealmLiveEntityObserver(monarchy.realmConfiguration) { - override val query = Monarchy.Query { GroupEntity.where(it) } + override val query = Monarchy.Query { GroupEntity.where(it) } override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - val newGroupIds = changeSet.insertions + // `insertions` for new groups and `changes` to handle left groups + val modifiedGroupEntity = (changeSet.insertions + changeSet.changes) .asSequence() - .mapNotNull { results[it]?.groupId} - .toList() + .mapNotNull { results[it] } + + fetchGroupsData(modifiedGroupEntity + .filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE } + .map { it.groupId } + .toList()) + + deleteGroups(modifiedGroupEntity + .filter { it.membership == Membership.LEAVE } + .map { it.groupId } + .toList()) + } + + private fun fetchGroupsData(groupIds: List) { + val getGroupDataWorkerParams = GetGroupDataWorker.Params(userId, groupIds) - val getGroupDataWorkerParams = GetGroupDataWorker.Params(credentials.userId, newGroupIds) val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) val sendWork = matrixOneTimeWorkRequestBuilder() @@ -61,4 +74,15 @@ internal class GroupSummaryUpdater @Inject constructor(private val context: Cont .enqueue() } + /** + * Delete the GroupSummaryEntity of left groups + */ + private fun deleteGroups(groupIds: List) { + monarchy + .writeAsync { realm -> + GroupSummaryEntity.where(realm, groupIds) + .findAll() + .deleteAllFromRealm() + } + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt index 83b89701b3b..fb436c3b21f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/DefaultPushRuleService.kt @@ -17,9 +17,10 @@ package im.vector.matrix.android.internal.session.notification import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.auth.data.SessionParams -import im.vector.matrix.android.api.pushrules.Action import im.vector.matrix.android.api.pushrules.PushRuleService +import im.vector.matrix.android.api.pushrules.RuleKind +import im.vector.matrix.android.api.pushrules.RuleSetKey +import im.vector.matrix.android.api.pushrules.getActions import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.util.Cancelable @@ -35,53 +36,60 @@ import timber.log.Timber import javax.inject.Inject @SessionScope -internal class DefaultPushRuleService @Inject constructor( - private val sessionParams: SessionParams, - private val pushRulesTask: GetPushRulesTask, - private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask, - private val taskExecutor: TaskExecutor, - private val monarchy: Monarchy +internal class DefaultPushRuleService @Inject constructor(private val getPushRulesTask: GetPushRulesTask, + private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask, + private val taskExecutor: TaskExecutor, + private val monarchy: Monarchy ) : PushRuleService { private var listeners = ArrayList() override fun fetchPushRules(scope: String) { - pushRulesTask + getPushRulesTask .configureWith(GetPushRulesTask.Params(scope)) .executeBy(taskExecutor) } override fun getPushRules(scope: String): List { - var contentRules: List = emptyList() var overrideRules: List = emptyList() var roomRules: List = emptyList() var senderRules: List = emptyList() var underrideRules: List = emptyList() - // TODO Create const for ruleSetKey monarchy.doWithRealm { realm -> - PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "content").findFirst()?.let { re -> - contentRules = re.pushRules.map { PushRulesMapper.mapContentRule(it) } - } - PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "override").findFirst()?.let { re -> - overrideRules = re.pushRules.map { PushRulesMapper.map(it) } - } - PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "room").findFirst()?.let { re -> - roomRules = re.pushRules.map { PushRulesMapper.mapRoomRule(it) } - } - PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "sender").findFirst()?.let { re -> - senderRules = re.pushRules.map { PushRulesMapper.mapSenderRule(it) } - } - PushRulesEntity.where(realm, sessionParams.credentials.userId, scope, "underride").findFirst()?.let { re -> - underrideRules = re.pushRules.map { PushRulesMapper.map(it) } - } + PushRulesEntity.where(realm, scope, RuleSetKey.CONTENT) + .findFirst() + ?.let { pushRulesEntity -> + contentRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapContentRule(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.OVERRIDE) + .findFirst() + ?.let { pushRulesEntity -> + overrideRules = pushRulesEntity.pushRules.map { PushRulesMapper.map(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.ROOM) + .findFirst() + ?.let { pushRulesEntity -> + roomRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapRoomRule(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.SENDER) + .findFirst() + ?.let { pushRulesEntity -> + senderRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapSenderRule(it) } + } + PushRulesEntity.where(realm, scope, RuleSetKey.UNDERRIDE) + .findFirst() + ?.let { pushRulesEntity -> + underrideRules = pushRulesEntity.pushRules.map { PushRulesMapper.map(it) } + } } - return contentRules + overrideRules + roomRules + senderRules + underrideRules + // Ref. for the order: https://matrix.org/docs/spec/client_server/latest#push-rules + return overrideRules + contentRules + roomRules + senderRules + underrideRules } - override fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable { + override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable { return updatePushRuleEnableStatusTask .configureWith(UpdatePushRuleEnableStatusTask.Params(kind, pushRule, enabled)) { this.callback = callback @@ -114,8 +122,9 @@ internal class DefaultPushRuleService @Inject constructor( fun dispatchBing(event: Event, rule: PushRule) { try { + val actionsList = rule.getActions() listeners.forEach { - it.onMatchRule(event, Action.mapFrom(rule) ?: emptyList()) + it.onMatchRule(event, actionsList) } } catch (e: Throwable) { Timber.e(e, "Error while dispatching bing") @@ -132,6 +141,16 @@ internal class DefaultPushRuleService @Inject constructor( } } + fun dispatchRedactedEventId(redactedEventId: String) { + try { + listeners.forEach { + it.onEventRedacted(redactedEventId) + } + } catch (e: Throwable) { + Timber.e(e, "Error while dispatching room left") + } + } + fun dispatchFinish() { try { listeners.forEach { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt index a434c6e9503..e2db0362bba 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/notification/ProcessEventForPushTask.kt @@ -16,11 +16,11 @@ package im.vector.matrix.android.internal.session.notification -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.RoomService +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.pushers.DefaultConditionResolver import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse import im.vector.matrix.android.internal.task.Task @@ -37,7 +37,7 @@ internal interface ProcessEventForPushTask : Task false } }.filter { - it.senderId != sessionParams.credentials.userId + it.senderId != userId } Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" + " to check for push rules with ${params.rules.size} rules") @@ -78,11 +78,31 @@ internal class DefaultProcessEventForPushTask @Inject constructor( defaultPushRuleService.dispatchBing(event, it) } } + + val allRedactedEvents = params.syncResponse.join + .map { entries -> + entries.value.timeline?.events?.filter { + it.type == EventType.REDACTION + } + .orEmpty() + .mapNotNull { it.redacts } + } + .fold(emptyList(), { acc, next -> + acc + next + }) + + Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events") + + allRedactedEvents.forEach { redactedEventId -> + defaultPushRuleService.dispatchRedactedEventId(redactedEventId) + } + defaultPushRuleService.dispatchFinish() } private fun fulfilledBingRule(event: Event, rules: List): PushRule? { - val conditionResolver = DefaultConditionResolver(event, roomService, sessionParams) + // TODO This should be injected + val conditionResolver = DefaultConditionResolver(event, roomService, userId) rules.filter { it.enabled }.forEach { rule -> val isFullfilled = rule.conditions?.map { it.asExecutableCondition()?.isSatisfied(conditionResolver) ?: false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt index bc587d91d10..e230c02e499 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddHttpPusherWorker.kt @@ -57,14 +57,14 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) return Result.failure() } return try { - setPusher(pusher, params.userId) + setPusher(pusher) Result.success() } catch (exception: Throwable) { when (exception) { is Failure.NetworkConnection -> Result.retry() else -> { monarchy.awaitTransaction { realm -> - PusherEntity.where(realm, params.userId, pusher.pushKey).findFirst()?.let { + PusherEntity.where(realm, pusher.pushKey).findFirst()?.let { //update it it.state = PusherState.FAILED_TO_REGISTER } @@ -76,12 +76,12 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) } } - private suspend fun setPusher(pusher: JsonPusher, userId: String) { + private suspend fun setPusher(pusher: JsonPusher) { executeRequest { apiCall = pushersAPI.setPusher(pusher) } monarchy.awaitTransaction { realm -> - val echo = PusherEntity.where(realm, userId, pusher.pushKey).findFirst() + val echo = PusherEntity.where(realm, pusher.pushKey).findFirst() if (echo != null) { //update it echo.appDisplayName = pusher.appDisplayName @@ -93,7 +93,7 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) echo.data?.url = pusher.data?.url echo.state = PusherState.REGISTERED } else { - pusher.toEntity(userId).also { + pusher.toEntity().also { it.state = PusherState.REGISTERED realm.insertOrUpdate(it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultConditionResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultConditionResolver.kt index a05cfb6cacd..a40c5e801b3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultConditionResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultConditionResolver.kt @@ -15,15 +15,16 @@ */ package im.vector.matrix.android.internal.session.pushers -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.pushrules.* import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.RoomService +import im.vector.matrix.android.internal.di.UserId import timber.log.Timber +// TODO Inject constructor internal class DefaultConditionResolver(private val event: Event, private val roomService: RoomService, - private val sessionParams: SessionParams) : ConditionResolver { + @UserId private val userId: String) : ConditionResolver { override fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean { @@ -45,8 +46,7 @@ internal class DefaultConditionResolver(private val event: Event, override fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition): Boolean { val roomId = event.roomId ?: return false val room = roomService.getRoom(roomId) ?: return false - val myDisplayName = room.getRoomMember(sessionParams.credentials.userId)?.displayName - ?: return false + val myDisplayName = room.getRoomMember(userId)?.displayName ?: return false return containsDisplayNameCondition.isSatisfied(event, myDisplayName) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt index 995d93311d7..12f6ee9f01d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt @@ -21,29 +21,28 @@ import androidx.work.BackoffPolicy import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.PusherEntity import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory -import java.util.UUID +import java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject -internal class DefaultPusherService @Inject constructor( - private val context: Context, - private val monarchy: Monarchy, - private val sessionParam: SessionParams, - private val getPusherTask: GetPushersTask, - private val removePusherTask: RemovePusherTask, - private val taskExecutor: TaskExecutor +internal class DefaultPusherService @Inject constructor(private val context: Context, + private val monarchy: Monarchy, + @UserId private val userId: String, + private val getPusherTask: GetPushersTask, + private val removePusherTask: RemovePusherTask, + private val taskExecutor: TaskExecutor ) : PushersService { @@ -70,7 +69,7 @@ internal class DefaultPusherService @Inject constructor( append = append) - val params = AddHttpPusherWorker.Params(pusher, sessionParam.credentials.userId) + val params = AddHttpPusherWorker.Params(pusher, userId) val request = matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerUtil.workConstraints) @@ -82,7 +81,7 @@ internal class DefaultPusherService @Inject constructor( } override fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback) { - val params = RemovePusherTask.Params(sessionParam.credentials.userId, pushkey, appId) + val params = RemovePusherTask.Params(pushkey, appId) removePusherTask .configureWith(params) { this.callback = callback @@ -93,12 +92,12 @@ internal class DefaultPusherService @Inject constructor( override fun livePushers(): LiveData> { return monarchy.findAllMappedWithChanges( - { realm -> PusherEntity.where(realm, sessionParam.credentials.userId) }, + { realm -> PusherEntity.where(realm) }, { it.asDomain() } ) } override fun pushers(): List { - return monarchy.fetchAllCopiedSync { PusherEntity.where(it, sessionParam.credentials.userId) }.map { it.asDomain() } + return monarchy.fetchAllCopiedSync { PusherEntity.where(it) }.map { it.asDomain() } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt index 627c6f89faa..d542a8fffc3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushRulesTask.kt @@ -15,80 +15,27 @@ */ package im.vector.matrix.android.internal.session.pushers -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse -import im.vector.matrix.android.internal.database.mapper.PushRulesMapper -import im.vector.matrix.android.internal.database.model.PushRulesEntity -import im.vector.matrix.android.internal.database.model.PusherEntityFields import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.util.awaitTransaction import javax.inject.Inject internal interface GetPushRulesTask : Task { - data class Params(val scope: String) - } - +/** + * We keep this task, but it should not be used anymore, the push rules comes from the sync response + */ internal class DefaultGetPushRulesTask @Inject constructor(private val pushRulesApi: PushRulesApi, - private val monarchy: Monarchy, - private val sessionParams: SessionParams) : GetPushRulesTask { + private val savePushRulesTask: SavePushRulesTask) : GetPushRulesTask { override suspend fun execute(params: GetPushRulesTask.Params) { val response = executeRequest { apiCall = pushRulesApi.getAllRules() } - val scope = params.scope - monarchy.awaitTransaction { realm -> - //clear existings? - //TODO - realm.where(PushRulesEntity::class.java) - .equalTo(PusherEntityFields.USER_ID, sessionParams.credentials.userId) - .findAll().deleteAllFromRealm() - - val content = PushRulesEntity(sessionParams.credentials.userId, scope, "content") - response.global.content?.forEach { rule -> - PushRulesMapper.map(rule).also { - content.pushRules.add(it) - } - } - realm.insertOrUpdate(content) - val override = PushRulesEntity(sessionParams.credentials.userId, scope, "override") - response.global.override?.forEach { rule -> - PushRulesMapper.map(rule).also { - override.pushRules.add(it) - } - } - realm.insertOrUpdate(override) - - val rooms = PushRulesEntity(sessionParams.credentials.userId, scope, "room") - response.global.room?.forEach { rule -> - PushRulesMapper.map(rule).also { - rooms.pushRules.add(it) - } - } - realm.insertOrUpdate(rooms) - - val senders = PushRulesEntity(sessionParams.credentials.userId, scope, "sender") - response.global.sender?.forEach { rule -> - PushRulesMapper.map(rule).also { - senders.pushRules.add(it) - } - } - realm.insertOrUpdate(senders) - - val underrides = PushRulesEntity(sessionParams.credentials.userId, scope, "underride") - response.global.underride?.forEach { rule -> - PushRulesMapper.map(rule).also { - underrides.pushRules.add(it) - } - } - realm.insertOrUpdate(underrides) - } + savePushRulesTask.execute(SavePushRulesTask.Params(response)) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt index b3199ea3707..8fd1a5b3bef 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/GetPushersTask.kt @@ -16,11 +16,9 @@ package im.vector.matrix.android.internal.session.pushers import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.pushers.PusherState import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.PusherEntity -import im.vector.matrix.android.internal.database.model.PusherEntityFields import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction @@ -29,8 +27,7 @@ import javax.inject.Inject internal interface GetPushersTask : Task internal class DefaultGetPusherTask @Inject constructor(private val pushersAPI: PushersAPI, - private val monarchy: Monarchy, - private val sessionParams: SessionParams) : GetPushersTask { + private val monarchy: Monarchy) : GetPushersTask { override suspend fun execute(params: Unit) { val response = executeRequest { @@ -39,10 +36,9 @@ internal class DefaultGetPusherTask @Inject constructor(private val pushersAPI: monarchy.awaitTransaction { realm -> //clear existings? realm.where(PusherEntity::class.java) - .equalTo(PusherEntityFields.USER_ID, sessionParams.credentials.userId) .findAll().deleteAllFromRealm() response.pushers?.forEach { jsonPusher -> - jsonPusher.toEntity(sessionParams.credentials.userId).also { + jsonPusher.toEntity().also { it.state = PusherState.REGISTERED realm.insertOrUpdate(it) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt index 0d00c9bbd6e..784a140b192 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt @@ -59,6 +59,9 @@ internal abstract class PushersModule { @Binds abstract fun bindGetPushRulesTask(getPushRulesTask: DefaultGetPushRulesTask): GetPushRulesTask + @Binds + abstract fun bindSavePushRulesTask(savePushRulesTask: DefaultSavePushRulesTask): SavePushRulesTask + @Binds abstract fun bindRemovePusherTask(removePusherTask: DefaultRemovePusherTask): RemovePusherTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt index 0ed7175e9a0..d22447586c9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePusherTask.kt @@ -28,8 +28,7 @@ import io.realm.Realm import javax.inject.Inject internal interface RemovePusherTask : Task { - data class Params(val userId: String, - val pushKey: String, + data class Params(val pushKey: String, val pushAppId: String) } @@ -40,12 +39,12 @@ internal class DefaultRemovePusherTask @Inject constructor( override suspend fun execute(params: RemovePusherTask.Params) { monarchy.awaitTransaction { realm -> - val existingEntity = PusherEntity.where(realm, params.userId, params.pushKey).findFirst() + val existingEntity = PusherEntity.where(realm, params.pushKey).findFirst() existingEntity?.state = PusherState.UNREGISTERING } val existing = Realm.getInstance(monarchy.realmConfiguration).use { realm -> - PusherEntity.where(realm, params.userId, params.pushKey).findFirst()?.asDomain() + PusherEntity.where(realm, params.pushKey).findFirst()?.asDomain() } ?: throw Exception("No existing pusher") val deleteBody = JsonPusher( @@ -64,7 +63,7 @@ internal class DefaultRemovePusherTask @Inject constructor( apiCall = pushersAPI.setPusher(deleteBody) } monarchy.awaitTransaction { - PusherEntity.where(it, params.userId, params.pushKey).findFirst()?.deleteFromRealm() + PusherEntity.where(it, params.pushKey).findFirst()?.deleteFromRealm() } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/SavePushRulesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/SavePushRulesTask.kt new file mode 100644 index 00000000000..b658858e478 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/SavePushRulesTask.kt @@ -0,0 +1,81 @@ +/* + * 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.pushers + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.pushrules.RuleScope +import im.vector.matrix.android.api.pushrules.RuleSetKey +import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse +import im.vector.matrix.android.internal.database.mapper.PushRulesMapper +import im.vector.matrix.android.internal.database.model.PushRulesEntity +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction +import javax.inject.Inject + + +/** + * Save the push rules in DB + */ +internal interface SavePushRulesTask : Task { + data class Params(val pushRules: GetPushRulesResponse) +} + +internal class DefaultSavePushRulesTask @Inject constructor(private val monarchy: Monarchy) : SavePushRulesTask { + + override suspend fun execute(params: SavePushRulesTask.Params) { + monarchy.awaitTransaction { realm -> + // clear current push rules + realm.where(PushRulesEntity::class.java) + .findAll() + .deleteAllFromRealm() + + // Save only global rules for the moment + val globalRules = params.pushRules.global + + val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT } + globalRules.content?.forEach { rule -> + content.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(content) + + val override = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.OVERRIDE } + globalRules.override?.forEach { rule -> + PushRulesMapper.map(rule).also { + override.pushRules.add(it) + } + } + realm.insertOrUpdate(override) + + val rooms = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.ROOM } + globalRules.room?.forEach { rule -> + rooms.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(rooms) + + val senders = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.SENDER } + globalRules.sender?.forEach { rule -> + senders.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(senders) + + val underrides = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.UNDERRIDE } + globalRules.underride?.forEach { rule -> + underrides.pushRules.add(PushRulesMapper.map(rule)) + } + realm.insertOrUpdate(underrides) + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt index 828d9e71aea..f3077e68371 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/UpdatePushRuleEnableStatusTask.kt @@ -15,6 +15,7 @@ */ package im.vector.matrix.android.internal.session.pushers +import im.vector.matrix.android.api.pushrules.RuleKind import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task @@ -22,7 +23,7 @@ import javax.inject.Inject internal interface UpdatePushRuleEnableStatusTask : Task { - data class Params(val kind: String, + data class Params(val kind: RuleKind, val pushRule: PushRule, val enabled: Boolean) } @@ -32,7 +33,7 @@ internal class DefaultUpdatePushRuleEnableStatusTask @Inject constructor(private override suspend fun execute(params: UpdatePushRuleEnableStatusTask.Params) { return executeRequest { - apiCall = pushRulesApi.updateEnableRuleStatus(params.kind, params.pushRule.ruleId, params.enabled) + apiCall = pushRulesApi.updateEnableRuleStatus(params.kind.value, params.pushRule.ruleId, params.enabled) } } } \ No newline at end of file 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 492dd035439..6b2a6843f16 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 @@ -25,6 +25,7 @@ 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.read.ReadService +import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService @@ -40,6 +41,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val roomSummaryMapper: RoomSummaryMapper, private val timelineService: TimelineService, private val sendService: SendService, + private val draftService: DraftService, private val stateService: StateService, private val readService: ReadService, private val cryptoService: CryptoService, @@ -48,6 +50,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, ) : Room, TimelineService by timelineService, SendService by sendService, + DraftService by draftService, StateService by stateService, ReadService by readService, RelationService by relationService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index 9d642d3a898..97bbe62b113 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -16,13 +16,13 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import io.realm.OrderedCollectionChangeSet @@ -38,7 +38,7 @@ import javax.inject.Inject */ internal class EventRelationsAggregationUpdater @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, - private val credentials: Credentials, + @UserId private val userId: String, private val task: EventRelationsAggregationTask, private val taskExecutor: TaskExecutor) : RealmLiveEntityObserver(realmConfiguration) { @@ -61,7 +61,7 @@ internal class EventRelationsAggregationUpdater @Inject constructor(@SessionData .toList() val params = EventRelationsAggregationTask.Params( insertedDomains, - credentials.userId + userId ) task.configureWith(params).executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt index 9161fb25a10..0b18279aa86 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomAvatarContent @@ -27,11 +26,12 @@ import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.room.membership.RoomMembers import javax.inject.Inject internal class RoomAvatarResolver @Inject constructor(private val monarchy: Monarchy, - private val credentials: Credentials) { + @UserId private val userId: String) { /** * Compute the room avatar url @@ -52,7 +52,7 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona if (members.size == 1) { res = members.firstOrNull()?.toRoomMember()?.avatarUrl } else if (members.size == 2) { - val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, credentials.userId).findFirst() + val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, userId).findFirst() res = firstOtherMember?.toRoomMember()?.avatarUrl } } 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 53da2d77098..e972f6a98ef 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 @@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper +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 @@ -38,6 +39,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona private val cryptoService: CryptoService, private val timelineServiceFactory: DefaultTimelineService.Factory, private val sendServiceFactory: DefaultSendService.Factory, + private val draftServiceFactory: DefaultDraftService.Factory, private val stateServiceFactory: DefaultStateService.Factory, private val readServiceFactory: DefaultReadService.Factory, private val relationServiceFactory: DefaultRelationService.Factory, @@ -51,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona roomSummaryMapper, timelineServiceFactory.create(roomId), sendServiceFactory.create(roomId), + draftServiceFactory.create(roomId), stateServiceFactory.create(roomId), readServiceFactory.create(roomId), cryptoService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index dda8b9322f6..f92f2ccc1e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.session.room -import im.vector.matrix.android.api.auth.data.Credentials +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership @@ -26,9 +26,11 @@ import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.query.isEventRead import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary @@ -37,9 +39,10 @@ import io.realm.Realm import io.realm.kotlin.createObject import javax.inject.Inject -internal class RoomSummaryUpdater @Inject constructor(private val credentials: Credentials, +internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId: String, private val roomDisplayNameResolver: RoomDisplayNameResolver, - private val roomAvatarResolver: RoomAvatarResolver) { + private val roomAvatarResolver: RoomAvatarResolver, + private val monarchy: Monarchy) { // TODO: maybe allow user of SDK to give that list private val PREVIEWABLE_TYPES = listOf( @@ -63,8 +66,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C membership: Membership? = null, roomSummary: RoomSyncSummary? = null, unreadNotifications: RoomSyncUnreadNotifications? = null) { - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) if (roomSummary != null) { if (roomSummary.heroes.isNotEmpty()) { @@ -85,12 +87,16 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C roomSummaryEntity.membership = membership } - val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES) + val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, includedTypes = PREVIEWABLE_TYPES) val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() + roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 + //avoid this call if we are sure there are unread events + || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) + val otherRoomMembers = RoomMembers(realm, roomId) .queryRoomMembersEvent() - .notEqualTo(EventEntityFields.STATE_KEY, credentials.userId) + .notEqualTo(EventEntityFields.STATE_KEY, userId) .findAll() .asSequence() .map { it.stateKey } @@ -98,9 +104,8 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.topic = lastTopicEvent?.content.toModel()?.topic - roomSummaryEntity.latestEvent = latestEvent + roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) - } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt new file mode 100644 index 00000000000..c5676f84c88 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt @@ -0,0 +1,166 @@ +/* + * 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.draft + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.BuildConfig +import im.vector.matrix.android.api.session.room.send.DraftService +import im.vector.matrix.android.api.session.room.send.UserDraft +import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.internal.database.mapper.DraftMapper +import im.vector.matrix.android.internal.database.model.DraftEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.model.UserDraftsEntity +import im.vector.matrix.android.internal.database.query.where +import io.realm.kotlin.createObject +import timber.log.Timber + +internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String, + private val monarchy: Monarchy +) : DraftService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): DraftService + } + + /** + * The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft, + * or even move an existing draft to the top of the list + */ + override fun saveDraft(draft: UserDraft) { + Timber.d("Draft: saveDraft ${privacySafe(draft)}") + + monarchy.writeAsync { realm -> + + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + + val userDraftsEntity = roomSummaryEntity.userDrafts + ?: realm.createObject().also { + roomSummaryEntity.userDrafts = it + } + + userDraftsEntity.let { userDraftEntity -> + // Save only valid draft + if (draft.isValid()) { + // Add a new draft or update the current one? + val newDraft = DraftMapper.map(draft) + + // Is it an update of the top draft? + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: create a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else if (topDraft.draftMode == DraftEntity.MODE_EDIT) { + // top draft is an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + if (topDraft.linkedEventId == newDraft.linkedEventId) { + // Update the top draft + Timber.d("Draft: update the top edit draft ${privacySafe(draft)}") + topDraft.content = newDraft.content + } else { + // Check a previously EDIT draft with the same id + val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find { + it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId + } + + if (existingEditDraftOfSameEvent != null) { + // Ignore the new text, restore what was typed before, by putting the draft to the top + Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent) + userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent) + } else { + Timber.d("Draft: add a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } + } else { + // Add a new regular draft to the top + Timber.d("Draft: add a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } + } else { + // Top draft is not an edit + if (newDraft.draftMode == DraftEntity.MODE_EDIT) { + Timber.d("Draft: create a new edit draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.add(newDraft) + } else { + // Update the top draft + Timber.d("Draft: update the top draft ${privacySafe(draft)}") + topDraft.draftMode = newDraft.draftMode + topDraft.content = newDraft.content + topDraft.linkedEventId = newDraft.linkedEventId + } + } + } else { + // There is no draft to save, so the composer was clear + Timber.d("Draft: delete a draft") + + val topDraft = userDraftEntity.userDrafts.lastOrNull() + + if (topDraft == null) { + Timber.d("Draft: nothing to do") + } else { + // Remove the top draft + Timber.d("Draft: remove the top draft") + userDraftEntity.userDrafts.remove(topDraft) + } + } + } + } + } + + private fun privacySafe(o: Any): Any { + if (BuildConfig.LOG_PRIVATE_DATA) { + return o + } + + return "" + } + + override fun deleteDraft() { + Timber.d("Draft: deleteDraft()") + + monarchy.writeAsync { realm -> + UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> + if (userDraftsEntity.userDrafts.isNotEmpty()) { + userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) + } + } + } + } + + override fun getDraftsLive(): LiveData> { + val liveData = RealmLiveData(monarchy.realmConfiguration) { + UserDraftsEntity.where(it, roomId) + } + + return Transformations.map(liveData) { userDraftsEntities -> + userDraftsEntities.firstOrNull()?.let { userDraftEntity -> + userDraftEntity.userDrafts.map { draftEntity -> + DraftMapper.map(draftEntity) + } + } ?: emptyList() + } + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 815fc96e8eb..37adba0df87 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.room.membership import android.content.Context import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.* @@ -30,6 +29,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import javax.inject.Inject /** @@ -37,7 +37,7 @@ import javax.inject.Inject */ internal class RoomDisplayNameResolver @Inject constructor(private val context: Context, private val monarchy: Monarchy, - private val credentials: Credentials + @UserId private val userId: String ) { /** @@ -79,7 +79,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: if (roomEntity?.membership == Membership.INVITE) { - val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst() + val inviteMeEvent = roomMembers.queryRoomMemberEvent(userId).findFirst() val inviterId = inviteMeEvent?.sender name = if (inviterId != null) { val inviterMemberEvent = loadedMembers.where() @@ -97,7 +97,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: } } else { loadedMembers.where() - .notEqualTo(EventEntityFields.STATE_KEY, credentials.userId) + .notEqualTo(EventEntityFields.STATE_KEY, userId) .limit(3) .findAll() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index c63733f3d29..ac3c7a4e229 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -17,13 +17,13 @@ package im.vector.matrix.android.internal.session.room.prune import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import io.realm.OrderedCollectionChangeSet @@ -37,7 +37,7 @@ import javax.inject.Inject * As it will actually delete the content, it should be called last in the list of listener. */ internal class EventsPruner @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, - private val credentials: Credentials, + @UserId private val userId: String, private val pruneEventTask: PruneEventTask, private val taskExecutor: TaskExecutor) : RealmLiveEntityObserver(realmConfiguration) { @@ -54,7 +54,7 @@ internal class EventsPruner @Inject constructor(@SessionDatabase realmConfigurat val params = PruneEventTask.Params( insertedDomains, - credentials.userId + userId ) pruneEventTask.configureWith(params).executeBy(taskExecutor) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index 505b9589113..67182e1501a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -22,17 +22,14 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity -import im.vector.matrix.android.internal.database.query.find -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.isEventRead import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -41,7 +38,7 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private private val taskExecutor: TaskExecutor, private val setReadMarkersTask: SetReadMarkersTask, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, - private val credentials: Credentials + @UserId private val userId: String ) : ReadService { @AssistedInject.Factory @@ -78,19 +75,7 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private override fun isEventRead(eventId: String): Boolean { - var isEventRead = false - monarchy.doWithRealm { - val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() - ?: return@doWithRealm - val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) - ?: return@doWithRealm - val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex - ?: Int.MIN_VALUE - val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex - ?: Int.MAX_VALUE - isEventRead = eventToCheckIndex <= readReceiptIndex - } - return isEventRead + return isEventRead(monarchy, userId, roomId, eventId) } override fun getEventReadReceiptsLive(eventId: String): LiveData> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 41c9cca507a..ac4712943d2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session.room.read import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity @@ -26,6 +25,7 @@ import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory @@ -48,7 +48,7 @@ private const val READ_MARKER = "m.fully_read" private const val READ_RECEIPT = "m.read" internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI, - private val credentials: Credentials, + @UserId private val userId: String, private val monarchy: Monarchy ) : SetReadMarkersTask { @@ -101,6 +101,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI ?: return@writeAsync roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 + roomSummary.hasUnreadMessages = false } } } @@ -108,7 +109,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI private fun isEventRead(roomId: String, eventId: String): Boolean { var isEventRead = false monarchy.doWithRealm { - val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst() + val readReceipt = ReadReceiptEntity.where(it, roomId, userId).findFirst() ?: return@doWithRealm val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId) ?: return@doWithRealm 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 0c8695cbcf5..a1abc94f297 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 @@ -23,7 +23,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary @@ -37,6 +36,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.RedactEventWorker @@ -50,7 +50,7 @@ import timber.log.Timber internal class DefaultRelationService @AssistedInject constructor(@Assisted private val roomId: String, private val context: Context, - private val credentials: Credentials, + @UserId private val userId: String, private val eventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, @@ -111,7 +111,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv //TODO duplicate with send service? private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest { val sendContentWorkerParams = RedactEventWorker.Params( - credentials.userId, + userId, localEvent.eventId!!, roomId, eventId, @@ -199,13 +199,13 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv private fun createEncryptEventWork(event: Event, keepKeys: List?): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(credentials.userId, roomId, event, keepKeys) + val params = EncryptEventWorker.Params(userId, roomId, event, keepKeys) val sendWorkData = WorkerParamsFactory.toData(params) return TimelineSendEventWorkCommon.createWork(sendWorkData, true) } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) + val sendContentWorkerParams = SendEventWorker.Params(userId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 2c20839b26d..a342d3fe72d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -17,22 +17,13 @@ package im.vector.matrix.android.internal.session.room.send import android.content.Context -import androidx.work.BackoffPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.Operation -import androidx.work.WorkManager +import androidx.work.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.isImageMessage -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.events.model.* import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.send.SendService @@ -46,6 +37,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.util.CancelableWork @@ -63,7 +55,7 @@ private const val BACKOFF_DELAY = 10_000L internal class DefaultSendService @AssistedInject constructor(@Assisted private val roomId: String, private val context: Context, - private val credentials: Credentials, + @UserId private val userId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, private val monarchy: Monarchy @@ -75,6 +67,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private } private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() + override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable { val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { saveLocalEcho(it) @@ -165,12 +158,10 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private override fun deleteFailedEcho(localEcho: TimelineEvent) { monarchy.writeAsync { realm -> - TimelineEventEntity.where(realm, eventId = localEcho.root.eventId - ?: "").findFirst()?.let { + TimelineEventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { it.deleteFromRealm() } - EventEntity.where(realm, eventId = localEcho.root.eventId - ?: "").findFirst()?.let { + EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { it.deleteFromRealm() } } @@ -297,7 +288,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(credentials.userId, roomId, event) + val params = EncryptEventWorker.Params(userId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(params) return matrixOneTimeWorkRequestBuilder() @@ -309,7 +300,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) + val sendContentWorkerParams = SendEventWorker.Params(userId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) @@ -319,7 +310,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { saveLocalEcho(it) } - val sendContentWorkerParams = RedactEventWorker.Params(credentials.userId, redactEvent.eventId!!, roomId, event.eventId, reason) + val sendContentWorkerParams = RedactEventWorker.Params(userId, redactEvent.eventId!!, roomId, event.eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return TimelineSendEventWorkCommon.createWork(redactWorkData, true) } @@ -328,7 +319,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private attachment: ContentAttachmentData, isRoomEncrypted: Boolean, startChain: Boolean): OneTimeWorkRequest { - val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment, isRoomEncrypted) + val uploadMediaWorkerParams = UploadContentWorker.Params(userId, roomId, event, attachment, isRoomEncrypted) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return matrixOneTimeWorkRequestBuilder() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index b0351dca4ff..ffc539471bc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.* @@ -33,12 +32,13 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.util.StringProvider import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer -import java.util.UUID +import java.util.* import javax.inject.Inject /** @@ -50,7 +50,7 @@ import javax.inject.Inject * * The transactionID is used as loc */ -internal class LocalEchoEventFactory @Inject constructor(private val credentials: Credentials, +internal class LocalEchoEventFactory @Inject constructor(@UserId private val userId: String, private val stringProvider: StringProvider, private val roomSummaryUpdater: RoomSummaryUpdater) { // TODO Inject @@ -163,7 +163,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), - senderId = credentials.userId, + senderId = userId, eventId = localId, type = EventType.REACTION, content = content.toContent(), @@ -255,7 +255,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), - senderId = credentials.userId, + senderId = userId, eventId = localID, type = EventType.MESSAGE, content = content.toContent(), @@ -373,7 +373,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), - senderId = credentials.userId, + senderId = userId, eventId = localID, type = EventType.REDACTION, redacts = eventId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index e4f48d35a45..efd099e9f96 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -32,8 +32,8 @@ internal interface GetContextOfEventTask : Task internal class DefaultSignOutTask @Inject constructor(private val context: Context, - private val credentials: Credentials, + @UserId private val userId: String, private val signOutAPI: SignOutAPI, private val sessionManager: SessionManager, private val sessionParamsStore: SessionParamsStore, @@ -55,13 +51,13 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte } Timber.d("SignOut: release session...") - sessionManager.releaseSession(credentials.userId) + sessionManager.releaseSession(userId) Timber.d("SignOut: cancel pending works...") WorkManagerUtil.cancelAllWorks(context) Timber.d("SignOut: delete session params...") - sessionParamsStore.delete(credentials.userId) + sessionParamsStore.delete(userId) Timber.d("SignOut: clear session data...") clearSessionDataTask.execute(Unit) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt index 29b119dd15f..9355384ef53 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt @@ -64,12 +64,13 @@ internal class GroupSyncHandler @Inject constructor(private val monarchy: Monarc } } + + /** Note: [im.vector.matrix.android.internal.session.group.GroupSummaryUpdater] is observing changes */ realm.insertOrUpdate(groups) } private fun handleJoinedGroup(realm: Realm, groupId: String): GroupEntity { - val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) groupEntity.membership = Membership.JOIN return groupEntity @@ -77,21 +78,16 @@ internal class GroupSyncHandler @Inject constructor(private val monarchy: Monarc private fun handleInvitedGroup(realm: Realm, groupId: String): GroupEntity { - val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) groupEntity.membership = Membership.INVITE return groupEntity } - // TODO : handle it private fun handleLeftGroup(realm: Realm, groupId: String): GroupEntity { - val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) groupEntity.membership = Membership.LEAVE return groupEntity } - - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index fccaa9b9b23..11ebff70481 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R +import im.vector.matrix.android.api.pushrules.RuleScope import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel @@ -77,11 +78,11 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private fun checkPushRules(roomsSyncResponse: RoomsSyncResponse) { Timber.v("[PushRules] --> checkPushRules") if (tokenStore.getLastToken() == null) { - Timber.v("[PushRules] <-- No push tule check on initial sync") + Timber.v("[PushRules] <-- No push rule check on initial sync") return } //nothing on initial sync - val rules = pushRuleService.getPushRules("global") + val rules = pushRuleService.getPushRules(RuleScope.GLOBAL) processForPushTask.configureWith(ProcessEventForPushTask.Params(roomsSyncResponse, rules)) .executeBy(taskExecutor) Timber.v("[PushRules] <-- Push task scheduled") @@ -113,12 +114,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private fun handleJoinedRoom(realm: Realm, roomId: String, roomSync: RoomSync, - isInitalSync: Boolean): RoomEntity { + isInitialSync: Boolean): RoomEntity { Timber.v("Handle join sync for room $roomId") if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { - handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync) + handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync) } if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt index 584e84c2ae0..1b843bda198 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.sync -import arrow.core.Try import im.vector.matrix.android.R import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService @@ -33,73 +32,70 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl private val cryptoService: DefaultCryptoService, private val initialSyncProgressService: DefaultInitialSyncProgressService) { - fun handleResponse(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean): Try { - return Try { - val isInitialSync = fromToken == null - Timber.v("Start handling sync, is InitialSync: $isInitialSync") - val reporter = initialSyncProgressService.takeIf { isInitialSync } + suspend fun handleResponse(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean) { + val isInitialSync = fromToken == null + Timber.v("Start handling sync, is InitialSync: $isInitialSync") + val reporter = initialSyncProgressService.takeIf { isInitialSync } + measureTimeMillis { + if (!cryptoService.isStarted()) { + Timber.v("Should start cryptoService") + cryptoService.start(isInitialSync) + } + }.also { + Timber.v("Finish handling start cryptoService in $it ms") + } + val measure = measureTimeMillis { + // Handle the to device events before the room ones + // to ensure to decrypt them properly measureTimeMillis { - if (!cryptoService.isStarted()) { - Timber.v("Should start cryptoService") - cryptoService.start(isInitialSync) + Timber.v("Handle toDevice") + reportSubtask(reporter, R.string.initial_sync_start_importing_account_crypto, 100, 0.1f) { + if (syncResponse.toDevice != null) { + cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) + } } }.also { - Timber.v("Finish handling start cryptoService in $it ms") + Timber.v("Finish handling toDevice in $it ms") } - val measure = measureTimeMillis { - // Handle the to device events before the room ones - // to ensure to decrypt them properly - measureTimeMillis { - Timber.v("Handle toDevice") - reportSubtask(reporter, R.string.initial_sync_start_importing_account_crypto, 100, 0.1f) { - if (syncResponse.toDevice != null) { - cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) - } - } - }.also { - Timber.v("Finish handling toDevice in $it ms") - } - measureTimeMillis { - Timber.v("Handle rooms") + measureTimeMillis { + Timber.v("Handle rooms") - reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) { - if (syncResponse.rooms != null) { - roomSyncHandler.handle(syncResponse.rooms, isInitialSync, reporter) - } + reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) { + if (syncResponse.rooms != null) { + roomSyncHandler.handle(syncResponse.rooms, isInitialSync, reporter) } - }.also { - Timber.v("Finish handling rooms in $it ms") } + }.also { + Timber.v("Finish handling rooms in $it ms") + } - measureTimeMillis { - reportSubtask(reporter, R.string.initial_sync_start_importing_account_groups, 100, 0.1f) { - Timber.v("Handle groups") - if (syncResponse.groups != null) { - groupSyncHandler.handle(syncResponse.groups, reporter) - } + measureTimeMillis { + reportSubtask(reporter, R.string.initial_sync_start_importing_account_groups, 100, 0.1f) { + Timber.v("Handle groups") + if (syncResponse.groups != null) { + groupSyncHandler.handle(syncResponse.groups, reporter) } - }.also { - Timber.v("Finish handling groups in $it ms") } + }.also { + Timber.v("Finish handling groups in $it ms") + } - measureTimeMillis { - reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) { - Timber.v("Handle accountData") - userAccountDataSyncHandler.handle(syncResponse.accountData, syncResponse.rooms?.invite) - } - }.also { - Timber.v("Finish handling accountData in $it ms") + measureTimeMillis { + reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) { + Timber.v("Handle accountData") + userAccountDataSyncHandler.handle(syncResponse.accountData, syncResponse.rooms?.invite) } - - Timber.v("On sync completed") - cryptoSyncHandler.onSyncCompleted(syncResponse) + }.also { + Timber.v("Finish handling accountData in $it ms") } - Timber.v("Finish handling sync in $measure ms") - syncResponse + + Timber.v("On sync completed") + cryptoSyncHandler.onSyncCompleted(syncResponse) } + Timber.v("Finish handling sync in $measure ms") } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt index ea4efa4740c..28d4d5fc48f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt @@ -18,10 +18,10 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.filter.FilterRepository @@ -36,7 +36,7 @@ internal interface SyncTask : Task { } internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, - private val credentials: Credentials, + @UserId private val userId: String, private val filterRepository: FilterRepository, private val syncResponseHandler: SyncResponseHandler, private val sessionParamsStore: SessionParamsStore, @@ -70,7 +70,7 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI, // Intercept 401 if (throwable is Failure.ServerError && throwable.error.code == MatrixError.UNKNOWN_TOKEN) { - sessionParamsStore.delete(credentials.userId) + sessionParamsStore.delete(userId) } throw throwable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index 6ea4693168f..87b4c2d1c13 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -17,16 +17,18 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.getDirectRooms import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.session.pushers.SavePushRulesTask import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages +import im.vector.matrix.android.internal.session.sync.model.UserAccountDataPushRules import im.vector.matrix.android.internal.session.sync.model.UserAccountDataSync import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask @@ -37,15 +39,17 @@ import timber.log.Timber import javax.inject.Inject internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy, - private val credentials: Credentials, + @UserId private val userId: String, private val directChatsHelper: DirectChatsHelper, private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val savePushRulesTask: SavePushRulesTask, private val taskExecutor: TaskExecutor) { - fun handle(accountData: UserAccountDataSync?, invites: Map?) { + suspend fun handle(accountData: UserAccountDataSync?, invites: Map?) { accountData?.list?.forEach { when (it) { is UserAccountDataDirectMessages -> handleDirectChatRooms(it) + is UserAccountDataPushRules -> handlePushRules(it) else -> return@forEach } } @@ -54,6 +58,10 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc } } + private suspend fun handlePushRules(userAccountDataPushRules: UserAccountDataPushRules) { + savePushRulesTask.execute(SavePushRulesTask.Params(userAccountDataPushRules.content)) + } + private fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) { monarchy.runTransactionSync { realm -> val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm) @@ -81,11 +89,11 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc val directChats = directChatsHelper.getLocalUserAccount() var hasUpdate = false invites.forEach { (roomId, _) -> - val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(credentials.userId) + val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(userId) val inviterId = myUserStateEvent?.sender val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() } val isDirect = myUserRoomMember?.isDirect - if (inviterId != null && inviterId != credentials.userId && isDirect == true) { + if (inviterId != null && inviterId != userId && isDirect == true) { directChats .getOrPut(inviterId, { arrayListOf() }) .apply { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt index 1cb65751614..f0b33809a1b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -30,7 +30,6 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.BackgroundDetectionObserver -import kotlinx.coroutines.CancellationException import timber.log.Timber import java.net.SocketTimeoutException import java.util.concurrent.CountDownLatch @@ -140,7 +139,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) { // Timeout are not critical Timber.v("Timeout") - } else if (failure is Failure.Unknown && failure.throwable is CancellationException) { + } else if (failure is Failure.Cancelled) { Timber.v("Cancelled") } else if (failure is Failure.ServerError && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountData.kt index ca5dbd1d372..1362eb9171f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountData.kt @@ -23,5 +23,6 @@ internal interface UserAccountData { const val TYPE_DIRECT_MESSAGES = "m.direct" const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls" const val TYPE_WIDGETS = "m.widgets" + const val TYPE_PUSH_RULES = "m.push_rules" } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataPushRules.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataPushRules.kt new file mode 100644 index 00000000000..e2bd12f79b2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataPushRules.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.matrix.android.internal.session.sync.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataPushRules( + @Json(name = "content") val content: GetPushRulesResponse +) : UserAccountData + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index 80fc4cc3b19..aaa82efef02 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.session.user.accountdata -import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.sync.model.UserAccountData import im.vector.matrix.android.internal.task.Task @@ -42,11 +42,11 @@ internal interface UpdateUserAccountDataTask : Task) { + override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + override fun joinRoom(roomId: String, viaServers: List, callback: MatrixCallback): Cancelable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } override fun getRoom(roomId: String): Room? { @@ -184,6 +202,53 @@ class PushrulesConditionTest { } class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room { + override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun deleteFailedEcho(localEcho: TimelineEvent) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun clearSendingQueue() { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun resendAllFailedMessages() { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun saveDraft(draft: UserDraft) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun deleteDraft() { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getDraftsLive(): LiveData> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getEventReadReceiptsLive(eventId: String): LiveData> { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getStateEvent(eventType: String): Event? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun editReply(replyToEdit: TimelineEvent, originalTimelineEvent: TimelineEvent, newBodyText: String, compatibilityBodyText: String): Cancelable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) { + } + override fun liveTimeLineEvent(eventId: String): LiveData { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } @@ -201,7 +266,7 @@ class PushrulesConditionTest { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } - override fun createTimeline(eventId: String?, allowedTypes: List?): Timeline { + override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } @@ -245,7 +310,7 @@ class PushrulesConditionTest { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } - override fun loadRoomMembersIfNeeded(): Cancelable { + override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } @@ -257,15 +322,15 @@ class PushrulesConditionTest { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } - override fun invite(userId: String, callback: MatrixCallback) { + override fun invite(userId: String, callback: MatrixCallback): Cancelable { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } - override fun join(callback: MatrixCallback) { + override fun join(viaServers: List, callback: MatrixCallback): Cancelable { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } - override fun leave(callback: MatrixCallback) { + override fun leave(callback: MatrixCallback): Cancelable { 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 866a95b7402..697d0f36d08 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 5 +ext.versionMinor = 6 ext.versionPatch = 0 static def getGitTimestamp() { @@ -252,8 +252,9 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0' // RXBinding - implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha2' - implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0-alpha2' + implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0' + implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0' + implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.0.0' implementation("com.airbnb.android:epoxy:$epoxy_version") kapt "com.airbnb.android:epoxy-processor:$epoxy_version" diff --git a/vector/src/fdroid/java/im/vector/riotx/fdroid/service/VectorSyncService.kt b/vector/src/fdroid/java/im/vector/riotx/fdroid/service/VectorSyncService.kt index a3f999a0016..b2a45eff817 100644 --- a/vector/src/fdroid/java/im/vector/riotx/fdroid/service/VectorSyncService.kt +++ b/vector/src/fdroid/java/im/vector/riotx/fdroid/service/VectorSyncService.kt @@ -21,11 +21,19 @@ import android.content.Intent import android.os.Build import im.vector.matrix.android.internal.session.sync.job.SyncService import im.vector.riotx.R +import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.features.notifications.NotificationUtils import timber.log.Timber class VectorSyncService : SyncService() { + private lateinit var notificationUtils: NotificationUtils + + override fun onCreate() { + super.onCreate() + notificationUtils = vectorComponent().notificationUtils() + } + override fun onDestroy() { removeForegroundNotif() super.onDestroy() @@ -43,7 +51,7 @@ class VectorSyncService : SyncService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Timber.v("VectorSyncService - onStartCommand ") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notification = NotificationUtils.buildForegroundServiceNotification(applicationContext, R.string.notification_listening_for_events, false) + val notification = notificationUtils.buildForegroundServiceNotification(R.string.notification_listening_for_events, false) startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) } return super.onStartCommand(intent, flags, startId) diff --git a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt index 5ad064683c8..cde848ea41e 100755 --- a/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector/src/gplay/java/im/vector/riotx/gplay/push/fcm/VectorFirebaseMessagingService.kt @@ -196,13 +196,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { // This ID can and should be used to detect duplicate notification requests. val eventId = data["event_id"] ?: return //Just ignore - val eventType = data["type"] if (eventType == null) { //Just add a generic unknown event val simpleNotifiableEvent = SimpleNotifiableEvent( session.myUserId, eventId, + null, true, //It's an issue in this case, all event will bing even if expected to be silent. title = getString(R.string.notification_unknown_new_event), description = "", @@ -213,10 +213,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { ) notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent) notificationDrawerManager.refreshNotificationDrawer() - - return } else { - val event = parseEvent(data) ?: return val notifiableEvent = notifiableEventResolver.resolveEvent(event, session) @@ -227,8 +224,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { Timber.e("--> ${event}") } } else { - - if (notifiableEvent is NotifiableMessageEvent) { if (TextUtils.isEmpty(notifiableEvent.senderName)) { notifiableEvent.senderName = data["sender_display_name"] @@ -245,7 +240,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { notificationDrawerManager.refreshNotificationDrawer() } } - } private fun findRoomNameBestEffort(data: Map, session: Session?): String? { diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index cd4a97327ee..bf56e71915f 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -48,7 +48,6 @@ import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.PushRuleTriggerListener -import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.version.VersionProvider @@ -73,6 +72,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var versionProvider: VersionProvider + @Inject lateinit var notificationUtils: NotificationUtils lateinit var vectorComponent: VectorComponent private var fontThreadHandler: Handler? = null @@ -112,7 +112,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. emojiCompatWrapper.init(fontRequest) - NotificationUtils.createNotificationChannels(applicationContext) + notificationUtils.createNotificationChannels() if (authenticator.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { val lastAuthenticatedSession = authenticator.getLastAuthenticatedSession()!! activeSessionHolder.setActiveSession(lastAuthenticatedSession) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 0549222428e..a76091fb360 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -27,6 +27,7 @@ import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.VectorApplication import im.vector.riotx.core.pushers.PushersManager +import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler @@ -36,10 +37,7 @@ import im.vector.riotx.features.home.HomeRoomListObservableStore import im.vector.riotx.features.home.group.SelectedGroupStore import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.navigation.Navigator -import im.vector.riotx.features.notifications.NotifiableEventResolver -import im.vector.riotx.features.notifications.NotificationBroadcastReceiver -import im.vector.riotx.features.notifications.NotificationDrawerManager -import im.vector.riotx.features.notifications.PushRuleTriggerListener +import im.vector.riotx.features.notifications.* import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler @@ -58,12 +56,16 @@ interface VectorComponent { fun currentSession(): Session + fun notificationUtils(): NotificationUtils + fun notificationDrawerManager(): NotificationDrawerManager fun appContext(): Context fun resources(): Resources + fun dimensionUtils(): DimensionConverter + fun vectorConfiguration(): VectorConfiguration fun avatarRenderer(): AvatarRenderer @@ -72,7 +74,7 @@ interface VectorComponent { fun emojiCompatFontProvider(): EmojiCompatFontProvider - fun emojiCompatWrapper() : EmojiCompatWrapper + fun emojiCompatWrapper(): EmojiCompatWrapper fun eventHtmlRenderer(): EventHtmlRenderer diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index bb7892e1092..d430c4bbcb7 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -20,9 +20,10 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider +import java.net.SocketTimeoutException import javax.inject.Inject -class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) { +class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) { fun toHumanReadable(failure: Failure): String { @@ -33,7 +34,13 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) { fun toHumanReadable(throwable: Throwable?): String { return when (throwable) { null -> null - is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network) + is Failure.NetworkConnection -> { + if (throwable.ioException is SocketTimeoutException) { + stringProvider.getString(R.string.error_network_timeout) + } else { + stringProvider.getString(R.string.error_no_network) + } + } is Failure.ServerError -> { if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) { // Special case for terms and conditions diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt index 95e17a4b7d8..713c7644906 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt @@ -32,7 +32,6 @@ fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener) startSync(isAtLeastStarted) refreshPushers() pushRuleTriggerListener.startWithSession(this) - fetchPushRules() // TODO P1 From HomeActivity // @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler diff --git a/vector/src/main/java/im/vector/riotx/core/services/CallService.kt b/vector/src/main/java/im/vector/riotx/core/services/CallService.kt index 92e9e40a557..fae7ae7246a 100644 --- a/vector/src/main/java/im/vector/riotx/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/riotx/core/services/CallService.kt @@ -19,6 +19,7 @@ package im.vector.riotx.core.services import android.content.Context import android.content.Intent import androidx.core.content.ContextCompat +import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.features.notifications.NotificationUtils import timber.log.Timber @@ -32,11 +33,18 @@ class CallService : VectorService() { */ private var mCallIdInProgress: String? = null + private lateinit var notificationUtils: NotificationUtils + /** * incoming (foreground notification) */ private var mIncomingCallId: String? = null + override fun onCreate() { + super.onCreate() + notificationUtils = vectorComponent().notificationUtils() + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) { // Service started again by the system. @@ -120,7 +128,7 @@ class CallService : VectorService() { private fun displayCallInProgressNotification(intent: Intent) { val callId = intent.getStringExtra(EXTRA_CALL_ID) - val notification = NotificationUtils.buildPendingCallNotification(applicationContext, + val notification = notificationUtils.buildPendingCallNotification( intent.getBooleanExtra(EXTRA_IS_VIDEO, false), intent.getStringExtra(EXTRA_ROOM_NAME), intent.getStringExtra(EXTRA_ROOM_ID), @@ -136,7 +144,7 @@ class CallService : VectorService() { * Hide the permanent call notifications */ private fun hideCallNotifications() { - val notification = NotificationUtils.buildCallEndedNotification(applicationContext) + val notification = notificationUtils.buildCallEndedNotification() // It's mandatory to startForeground to avoid crash startForeground(NOTIFICATION_ID, notification) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DimensionUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt similarity index 77% rename from vector/src/main/java/im/vector/riotx/core/utils/DimensionUtils.kt rename to vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt index 15257f9c5db..3b8e3ed5cb4 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/DimensionUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt @@ -15,25 +15,26 @@ */ package im.vector.riotx.core.utils -import android.content.Context +import android.content.res.Resources import android.util.TypedValue +import javax.inject.Inject -object DimensionUtils { +class DimensionConverter @Inject constructor(val resources: Resources) { - fun dpToPx(dp: Int, context: Context): Int { + fun dpToPx(dp: Int): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), - context.resources.displayMetrics + resources.displayMetrics ).toInt() } - fun spToPx(sp: Int, context: Context): Int { + fun spToPx(sp: Int): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, sp.toFloat(), - context.resources.displayMetrics + resources.displayMetrics ).toInt() } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt index 9c7b7938256..75bc488571f 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt @@ -18,11 +18,7 @@ package im.vector.riotx.core.utils import android.annotation.TargetApi import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent +import android.content.* import android.net.Uri import android.os.Build import android.os.PowerManager @@ -32,7 +28,7 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import im.vector.riotx.R -import im.vector.riotx.features.notifications.supportNotificationChannels +import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.settings.VectorLocale import timber.log.Timber import java.util.* @@ -138,7 +134,7 @@ fun startNotificationSettingsIntent(activity: AppCompatActivity, requestCode: In */ @TargetApi(Build.VERSION_CODES.O) fun startNotificationChannelSettingsIntent(fragment: Fragment, channelID: String) { - if (!supportNotificationChannels()) return + if (!NotificationUtils.supportNotificationChannels()) return val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { putExtra(Settings.EXTRA_APP_PACKAGE, fragment.context?.packageName) putExtra(Settings.EXTRA_CHANNEL_ID, channelID) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index 95e59007bb1..48eec62ecc7 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -34,8 +34,8 @@ import im.vector.riotx.core.ui.list.genericItem import java.util.* import javax.inject.Inject -class KeysBackupSettingsRecyclerViewController @Inject constructor(val stringProvider: StringProvider, - val session: Session) : TypedEpoxyController() { +class KeysBackupSettingsRecyclerViewController @Inject constructor(private val stringProvider: StringProvider, + private val session: Session) : TypedEpoxyController() { var listener: Listener? = null diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt index 83e6361252a..1e2256825fb 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt @@ -54,7 +54,7 @@ import kotlin.collections.HashMap */ @Singleton -class KeyRequestHandler @Inject constructor(val context: Context) +class KeyRequestHandler @Inject constructor(private val context: Context) : RoomKeysRequestListener, SasVerificationService.SasVerificationListener { @@ -204,7 +204,7 @@ class KeyRequestHandler @Inject constructor(val context: Context) Runnable { alert.weakCurrentActivity?.get()?.let { val intent = SASVerificationActivity.outgoingIntent(it, - session?.sessionParams?.credentials?.userId ?: "", + session?.myUserId ?: "", userId, deviceId) it.startActivity(intent) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt index e138eb9ea4e..a77ce65c63d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -29,7 +29,7 @@ import javax.inject.Singleton * Listens to the VerificationManager and add a new notification when an incoming request is detected. */ @Singleton -class IncomingVerificationRequestHandler @Inject constructor(val context: Context) : SasVerificationService.SasVerificationListener { +class IncomingVerificationRequestHandler @Inject constructor(private val context: Context) : SasVerificationService.SasVerificationListener { private var session: Session? = null @@ -60,7 +60,7 @@ class IncomingVerificationRequestHandler @Inject constructor(val context: Contex .apply { contentAction = Runnable { val intent = SASVerificationActivity.incomingIntent(context, - session?.sessionParams?.credentials?.userId ?: "", + session?.myUserId ?: "", tx.otherUserId, tx.transactionId) weakCurrentActivity?.get()?.startActivity(intent) @@ -78,7 +78,7 @@ class IncomingVerificationRequestHandler @Inject constructor(val context: Contex context.getString(R.string.action_open), Runnable { val intent = SASVerificationActivity.incomingIntent(context, - session?.sessionParams?.credentials?.userId ?: "", + session?.myUserId ?: "", tx.otherUserId, tx.transactionId) weakCurrentActivity?.get()?.startActivity(intent) diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt index 77473366276..c7aeed645b7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt @@ -36,7 +36,7 @@ import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.setupAsSearch import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.core.utils.DimensionUtils +import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.home.AvatarRenderer import kotlinx.android.synthetic.main.fragment_create_direct_room.* import javax.inject.Inject @@ -51,6 +51,7 @@ class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersContr @Inject lateinit var directRoomController: KnownUsersController @Inject lateinit var avatarRenderer: AvatarRenderer + @Inject lateinit var dimensionConverter: DimensionConverter private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel override fun injectWith(injector: ScreenComponent) { @@ -156,7 +157,7 @@ class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersContr private fun addChipToGroup(user: User, chipGroup: ChipGroup) { val chip = Chip(requireContext()) chip.setChipBackgroundColorResource(android.R.color.transparent) - chip.chipStrokeWidth = DimensionUtils.dpToPx(1, requireContext()).toFloat() + chip.chipStrokeWidth = dimensionConverter.dpToPx(1).toFloat() chip.text = if (user.displayName.isNullOrBlank()) user.userId else user.displayName chip.isClickable = true chip.isCheckable = false diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt index 7aff4a327de..0be22e411e6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt @@ -26,6 +26,7 @@ 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.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.rx.rx import im.vector.riotx.R import im.vector.riotx.core.extensions.postLiveEvent @@ -93,20 +94,20 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro session .rx() .liveGroupSummaries() + // Keep only joined groups. Group invitations will be managed later + .map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } } .map { val myUser = session.getUser(session.myUserId) val allCommunityGroup = GroupSummary( groupId = ALL_COMMUNITIES_GROUP_ID, + membership = Membership.JOIN, displayName = stringProvider.getString(R.string.group_all_communities), avatarUrl = myUser?.avatarUrl ?: "") listOf(allCommunityGroup) + it } .execute { async -> - // TODO Phase2 Handle the case where the selected group is deleted on another client val newSelectedGroup = selectedGroup ?: async()?.firstOrNull() copy(asyncGroups = async, selectedGroup = newSelectedGroup) } } - - } \ No newline at end of file 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 e60bc422a8a..4aeb4f973a8 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 @@ -18,13 +18,13 @@ package im.vector.riotx.features.home.room.detail import com.jaiselrahman.filepicker.model.MediaFile import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent sealed class RoomDetailActions { + data class SaveDraft(val draft: String) : RoomDetailActions() data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : RoomDetailActions() data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() @@ -35,13 +35,15 @@ sealed class RoomDetailActions { data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() - data class HandleTombstoneEvent(val event: Event): RoomDetailActions() + data class HandleTombstoneEvent(val event: Event) : RoomDetailActions() object AcceptInvite : RoomDetailActions() object RejectInvite : RoomDetailActions() - data class EnterEditMode(val eventId: String) : RoomDetailActions() - data class EnterQuoteMode(val eventId: String) : RoomDetailActions() - data class EnterReplyMode(val eventId: String) : RoomDetailActions() + data class EnterEditMode(val eventId: String, val draft: String) : RoomDetailActions() + data class EnterQuoteMode(val eventId: String, val draft: String) : RoomDetailActions() + data class EnterReplyMode(val eventId: String, val draft: String) : RoomDetailActions() + data class ExitSpecialMode(val draft: String) : RoomDetailActions() + data class ResendMessage(val eventId: String) : RoomDetailActions() data class RemoveFailedEcho(val eventId: String) : RoomDetailActions() object ClearSendQueue : 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 cd1ccb01d24..7bc5cf70167 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 @@ -64,7 +64,6 @@ import im.vector.matrix.android.api.session.room.model.message.* 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.getTextEditableContent import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent @@ -187,7 +186,6 @@ class RoomDetailFragment : @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer @Inject lateinit var vectorPreferences: VectorPreferences - override fun getLayoutResId() = R.layout.fragment_room_detail override fun getMenuRes() = R.menu.menu_timeline @@ -197,6 +195,8 @@ class RoomDetailFragment : @BindView(R.id.composerLayout) lateinit var composerLayout: TextComposerView + private var lockSendButton = false + override fun injectWith(injector: ScreenComponent) { injector.inject(this) } @@ -242,10 +242,10 @@ class RoomDetailFragment : roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode -> when (mode) { - SendMode.REGULAR -> exitSpecialMode() - is SendMode.EDIT -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_edit, true) - is SendMode.QUOTE -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_quote, false) - is SendMode.REPLY -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_reply, false) + is SendMode.REGULAR -> renderRegularMode(mode.text) + is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, mode.text) + is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, mode.text) + is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, mode.text) } } @@ -300,14 +300,16 @@ class RoomDetailFragment : return super.onOptionsItemSelected(item) } - private fun exitSpecialMode() { + private fun renderRegularMode(text: String) { commandAutocompletePolicy.enabled = true composerLayout.collapse() + + updateComposerText(text) } - private fun enterSpecialMode(event: TimelineEvent, - @DrawableRes iconRes: Int, - useText: Boolean) { + private fun renderSpecialMode(event: TimelineEvent, + @DrawableRes iconRes: Int, + defaultContent: String) { commandAutocompletePolicy.enabled = false //switch to expanded bar composerLayout.composerRelatedMessageTitle.apply { @@ -321,19 +323,20 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } - composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody + + updateComposerText(defaultContent) - composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) - avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + avatarRenderer.render(event.senderAvatar, + event.root.senderId ?: "", + event.senderName, + composerLayout.composerRelatedMessageAvatar) - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { //need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -341,6 +344,15 @@ class RoomDetailFragment : focusComposerAndShowKeyboard() } + private fun updateComposerText(text: String) { + // Do not update if this is the same text to avoid the cursor to move + if (text != composerLayout.composerEditText.text.toString()) { + // Ignore update to avoid saving a draft + composerLayout.composerEditText.setText(text) + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) + } + } + override fun onResume() { super.onResume() @@ -351,6 +363,8 @@ class RoomDetailFragment : super.onPause() notificationDrawerManager.setCurrentRoom(null) + + roomDetailViewModel.process(RoomDetailActions.SaveDraft(composerLayout.composerEditText.text.toString())) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -360,9 +374,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -397,26 +411,26 @@ class RoomDetailFragment : if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } - - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) + } + } + + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } @@ -486,14 +500,18 @@ class RoomDetailFragment : .build() composerLayout.sendButton.setOnClickListener { + if (lockSendButton) { + Timber.w("Send button is locked") + return@setOnClickListener + } val textMessage = composerLayout.composerEditText.text.toString() if (textMessage.isNotBlank()) { + lockSendButton = true roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, vectorPreferences.isMarkdownEnabled())) } } composerLayout.composerRelatedMessageCloseButton.setOnClickListener { - composerLayout.composerEditText.setText("") - roomDetailViewModel.resetSendMode() + roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString())) } } @@ -645,13 +663,11 @@ class RoomDetailFragment : private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { when (sendMessageResult) { is SendMessageResult.MessageSent -> { - // Clear composer - composerLayout.composerEditText.text = null + updateComposerText("") } is SendMessageResult.SlashCommandHandled -> { sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } - // Clear composer - composerLayout.composerEditText.text = null + updateComposerText("") } is SendMessageResult.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) @@ -660,7 +676,7 @@ class RoomDetailFragment : displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } is SendMessageResult.SlashCommandResultOk -> { - // Ignore + updateComposerText("") } is SendMessageResult.SlashCommandResultError -> { displayCommandError(sendMessageResult.throwable.localizedMessage) @@ -669,6 +685,8 @@ class RoomDetailFragment : displayCommandError(getString(R.string.not_implemented)) } } + + lockSendButton = false } private fun displayCommandError(message: String) { @@ -913,13 +931,13 @@ class RoomDetailFragment : roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } is SimpleAction.Edit -> { - roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId)) + roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString())) } is SimpleAction.Quote -> { - roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId)) + roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString())) } is SimpleAction.Reply -> { - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId)) + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString())) } is SimpleAction.CopyPermalink -> { val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) 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 1cd8cc4a41e..80f333a76ea 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 @@ -42,8 +42,9 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx @@ -109,6 +110,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeRoomSummary() observeEventDisplayedActions() observeSummaryState() + observeDrafts() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } @@ -116,6 +118,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro fun process(action: RoomDetailActions) { when (action) { + is RoomDetailActions.SaveDraft -> handleSaveDraft(action) is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) @@ -129,6 +132,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailActions.EnterEditMode -> handleEditAction(action) is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) + is RoomDetailActions.ExitSpecialMode -> handleExitSpecialMode(action) is RoomDetailActions.DownloadFile -> handleDownloadFile(action) is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action) @@ -140,9 +144,54 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + /** + * Convert a send mode to a draft and save the draft + */ + private fun handleSaveDraft(action: RoomDetailActions.SaveDraft) { + withState { + when (it.sendMode) { + is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft)) + is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft)) + } + } + } + + private fun observeDrafts() { + room.rx().liveDrafts() + .subscribe { + Timber.d("Draft update --> SetState") + setState { + val draft = it.lastOrNull() ?: UserDraft.REGULAR("") + copy( + // Create a sendMode from a draft and retrieve the TimelineEvent + sendMode = when (draft) { + is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, draft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, draft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, draft.text) + } + } + } ?: SendMode.REGULAR("") + ) + } + } + .disposeOnClear() + } + private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { - val tombstoneContent = action.event.getClearContent().toModel() - ?: return + val tombstoneContent = action.event.getClearContent().toModel() ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -166,22 +215,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } - private fun enterEditMode(event: TimelineEvent) { - setState { - copy( - sendMode = SendMode.EDIT(event) - ) - } - } - - fun resetSendMode() { - setState { - copy( - sendMode = SendMode.REGULAR - ) - } - } - private val _nonBlockingPopAlert = MutableLiveData>>>() val nonBlockingPopAlert: LiveData>>> get() = _nonBlockingPopAlert @@ -218,7 +251,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleSendMessage(action: RoomDetailActions.SendMessage) { withState { state -> when (state.sendMode) { - SendMode.REGULAR -> { + is SendMode.REGULAR -> { val slashCommandResult = CommandParser.parseSplashCommand(action.text) when (slashCommandResult) { @@ -226,6 +259,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // Send the text message to the room room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) + popDraft() } is ParsedCommand.ErrorSyntax -> { _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)) @@ -238,6 +272,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) + popDraft() } is ParsedCommand.SetUserPowerLevel -> { // TODO @@ -251,6 +286,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled( if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) + popDraft() } is ParsedCommand.UnbanUser -> { // TODO @@ -275,9 +311,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is ParsedCommand.SendEmote -> { room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) + popDraft() } is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) + popDraft() } is ParsedCommand.ChangeDisplayName -> { // TODO @@ -285,11 +323,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } } - is SendMode.EDIT -> { - + is SendMode.EDIT -> { //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -298,27 +335,24 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { - room.editTextMessage(state.sendMode.timelineEvent.root.eventId - ?: "", messageContent?.type - ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown) + room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", + messageContent?.type ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } } - setState { - copy( - sendMode = SendMode.REGULAR - ) - } _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) + popDraft() } - is SendMode.QUOTE -> { + is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -333,29 +367,24 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { room.sendFormattedTextMessage(finalText, htmlText) } - setState { - copy( - sendMode = SendMode.REGULAR - ) - } _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) + popDraft() } - is SendMode.REPLY -> { + is SendMode.REPLY -> { state.sendMode.timelineEvent.let { room.replyToMessage(it, action.text, action.autoMarkdown) - setState { - copy( - sendMode = SendMode.REGULAR - ) - } _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) + popDraft() } - } } } } + private fun popDraft() { + room.deleteDraft() + } + private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() var quotedTextMsg = StringBuilder() @@ -469,27 +498,71 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleEditAction(action: RoomDetailActions.EnterEditMode) { - room.getTimeLineEvent(action.eventId)?.let { - enterEditMode(it) + saveCurrentDraft(action.draft) + + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + timelineEvent.root.eventId?.let { + room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: "")) + } } } private fun handleQuoteAction(action: RoomDetailActions.EnterQuoteMode) { - room.getTimeLineEvent(action.eventId)?.let { - setState { - copy( - sendMode = SendMode.QUOTE(it) - ) + saveCurrentDraft(action.draft) + + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + withState { state -> + // Save a new draft and keep the previously entered text, if it was not an edit + timelineEvent.root.eventId?.let { + if (state.sendMode is SendMode.EDIT) { + room.saveDraft(UserDraft.QUOTE(it, "")) + } else { + room.saveDraft(UserDraft.QUOTE(it, action.draft)) + } + } } } } private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) { - room.getTimeLineEvent(action.eventId)?.let { - setState { - copy( - sendMode = SendMode.REPLY(it) - ) + saveCurrentDraft(action.draft) + + room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + withState { state -> + // Save a new draft and keep the previously entered text, if it was not an edit + timelineEvent.root.eventId?.let { + if (state.sendMode is SendMode.EDIT) { + room.saveDraft(UserDraft.REPLY(it, "")) + } else { + room.saveDraft(UserDraft.REPLY(it, action.draft)) + } + } + } + } + } + + private fun saveCurrentDraft(draft: String) { + // Save the draft with the current text if any + withState { + if (draft.isNotBlank()) { + when (it.sendMode) { + is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft)) + is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft)) + is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft)) + is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft)) + } + } + } + } + + private fun handleExitSpecialMode(action: RoomDetailActions.ExitSpecialMode) { + withState { state -> + // For edit, just delete the current draft + if (state.sendMode is SendMode.EDIT) { + room.deleteDraft() + } else { + // Save a new draft and keep the previously entered text + room.saveDraft(UserDraft.REGULAR(action.draft)) } } } @@ -658,7 +731,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { - summary.latestEvent?.root?.senderId?.let { senderId -> + summary.latestPreviewableEvent?.root?.senderId?.let { senderId -> session.getUser(senderId) }?.also { setState { copy(asyncInviter = Success(it)) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index d8358efe164..a47ee565003 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -34,11 +34,11 @@ import im.vector.matrix.android.api.session.user.model.User * * Depending on the state the bottom toolbar will change (icons/preview/actions...) */ -sealed class SendMode { - object REGULAR : SendMode() - data class QUOTE(val timelineEvent: TimelineEvent) : SendMode() - data class EDIT(val timelineEvent: TimelineEvent) : SendMode() - data class REPLY(val timelineEvent: TimelineEvent) : SendMode() +sealed class SendMode(open val text: String) { + data class REGULAR(override val text: String) : SendMode(text) + data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) + data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) + data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) } data class RoomDetailViewState( @@ -47,7 +47,7 @@ data class RoomDetailViewState( val timeline: Timeline? = null, val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, - val sendMode: SendMode = SendMode.REGULAR, + val sendMode: SendMode = SendMode.REGULAR(""), val isEncrypted: Boolean = false, val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 080565cd161..9c4c59c46c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -25,6 +25,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel 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.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ @@ -36,7 +37,8 @@ import javax.inject.Inject class EncryptedItemFactory @Inject constructor(private val messageInformationDataFactory: MessageInformationDataFactory, private val colorProvider: ColorProvider, private val stringProvider: StringProvider, - private val avatarRenderer: AvatarRenderer) { + private val avatarRenderer: AvatarRenderer, + private val dimensionConverter: DimensionConverter) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?, @@ -69,6 +71,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat .message(spannableStr) .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) + .dimensionConverter(dimensionConverter) .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 4a3f50c45e3..58804f5f32d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar @@ -33,7 +34,8 @@ import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ import javax.inject.Inject class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider, - private val avatarRenderer: AvatarRenderer) { + private val avatarRenderer: AvatarRenderer, + private val dimensionConverter: DimensionConverter) { fun create(event: TimelineEvent, highlight: Boolean, @@ -50,6 +52,7 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri ) return NoticeItem_() .avatarRenderer(avatarRenderer) + .dimensionConverter(dimensionConverter) .noticeText(text) .informationData(informationData) .highlighted(highlight) 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 3c636bfa58d..4819db4075d 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 @@ -40,6 +40,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.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder @@ -62,7 +64,8 @@ class MessageItemFactory @Inject constructor( private val imageContentRenderer: ImageContentRenderer, private val messageInformationDataFactory: MessageInformationDataFactory, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, - private val noticeItemFactory: NoticeItemFactory) { + private val noticeItemFactory: NoticeItemFactory, + private val dimensionConverter: DimensionConverter) { fun create(event: TimelineEvent, @@ -113,6 +116,7 @@ class MessageItemFactory @Inject constructor( return MessageFileItem_() .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) + .dimensionConverter(dimensionConverter) .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) @@ -142,6 +146,7 @@ class MessageItemFactory @Inject constructor( return MessageFileItem_() .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) + .dimensionConverter(dimensionConverter) .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) @@ -171,6 +176,7 @@ class MessageItemFactory @Inject constructor( return DefaultItem_() .text(text) .avatarRenderer(avatarRenderer) + .dimensionConverter(dimensionConverter) .highlighted(highlight) .informationData(informationData) .baseCallback(callback) @@ -197,6 +203,7 @@ class MessageItemFactory @Inject constructor( return MessageImageVideoItem_() .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) + .dimensionConverter(dimensionConverter) .imageContentRenderer(imageContentRenderer) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .playable(messageContent.info?.mimeType == "image/gif") @@ -251,6 +258,7 @@ class MessageItemFactory @Inject constructor( .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) + .dimensionConverter(dimensionConverter) .playable(true) .informationData(informationData) .highlighted(highlight) @@ -290,9 +298,11 @@ class MessageItemFactory @Inject constructor( message(linkifiedBody) } } + .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .avatarRenderer(avatarRenderer) .informationData(informationData) .colorProvider(colorProvider) + .dimensionConverter(dimensionConverter) .highlighted(highlight) .avatarCallback(callback) .urlClickCallback(callback) @@ -326,7 +336,8 @@ 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) @@ -359,6 +370,7 @@ class MessageItemFactory @Inject constructor( .avatarRenderer(avatarRenderer) .message(message) .colorProvider(colorProvider) + .dimensionConverter(dimensionConverter) .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) @@ -400,6 +412,7 @@ class MessageItemFactory @Inject constructor( } .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) + .dimensionConverter(dimensionConverter) .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) @@ -423,6 +436,7 @@ class MessageItemFactory @Inject constructor( return RedactedMessageItem_() .avatarRenderer(avatarRenderer) .colorProvider(colorProvider) + .dimensionConverter(dimensionConverter) .informationData(informationData) .highlighted(highlight) .avatarCallback(callback) @@ -447,4 +461,8 @@ class MessageItemFactory @Inject constructor( VectorLinkify.addLinks(spannable, true) return spannable } + + companion object { + private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5 + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index f73a200133b..76ac0e70f5b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -17,12 +17,10 @@ package im.vector.riotx.features.home.room.detail.timeline.factory import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.helper.senderName -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory @@ -30,7 +28,8 @@ import javax.inject.Inject class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, private val avatarRenderer: AvatarRenderer, - private val informationDataFactory: MessageInformationDataFactory) { + private val informationDataFactory: MessageInformationDataFactory, + private val dimensionConverter: DimensionConverter) { fun create(event: TimelineEvent, highlight: Boolean, @@ -40,6 +39,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv return NoticeItem_() .avatarRenderer(avatarRenderer) + .dimensionConverter(dimensionConverter) .noticeText(formattedText) .highlighted(highlight) .informationData(informationData) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 2a4142e474f..8cc181bd37c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -33,7 +33,6 @@ import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.DebouncedClickListener -import im.vector.riotx.core.utils.DimensionUtils.dpToPx import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.reactions.widget.ReactionButton @@ -100,7 +99,7 @@ abstract class AbsMessageItem : BaseEventItem() { super.bind(holder) if (informationData.showInformation) { holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply { - val size = dpToPx(avatarStyle.avatarSizeDP, holder.view.context) + val size = dimensionConverter.dpToPx(avatarStyle.avatarSizeDP) height = size width = size } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 96625d16b82..561059de632 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -25,7 +25,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.platform.CheckableView import im.vector.riotx.core.ui.views.ReadReceiptsView -import im.vector.riotx.core.utils.DimensionUtils.dpToPx +import im.vector.riotx.core.utils.DimensionConverter /** * Children must override getViewType() @@ -38,10 +38,13 @@ abstract class BaseEventItem : VectorEpoxyModel @EpoxyAttribute var highlighted: Boolean = false + @EpoxyAttribute + lateinit var dimensionConverter: DimensionConverter + override fun bind(holder: H) { super.bind(holder) //optimize? - val px = dpToPx(avatarStyle.avatarSizeDP + 8, holder.view.context) + val px = dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + 8) holder.leftGuideline.setGuidelineBegin(px) holder.checkableBackground.isChecked = highlighted diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt index fc867b1277e..3d682cdde36 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -24,7 +24,6 @@ import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.core.utils.containsOnlyEmojis import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.html.PillImageSpan import kotlinx.coroutines.Dispatchers @@ -39,6 +38,8 @@ abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute var message: CharSequence? = null @EpoxyAttribute + var useBigFont: Boolean = false + @EpoxyAttribute var urlClickCallback: TimelineEventController.UrlClickCallback? = null // Better link movement methods fixes the issue when @@ -65,9 +66,7 @@ abstract class MessageTextItem : AbsMessageItem() { super.bind(holder) holder.messageView.movementMethod = mvmtMethod - - val msg = message ?: "" - if (msg.length <= 4 && containsOnlyEmojis(msg.toString())) { + if (useBigFont) { holder.messageView.textSize = 44F } else { holder.messageView.textSize = 14F diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/ChronologicalRoomComparator.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/ChronologicalRoomComparator.kt index c4a1633bed5..d25198f56a3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/ChronologicalRoomComparator.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/ChronologicalRoomComparator.kt @@ -25,14 +25,14 @@ class ChronologicalRoomComparator @Inject constructor() : Comparator() { @EpoxyAttribute lateinit var title: CharSequence @EpoxyAttribute var expanded: Boolean = false - @EpoxyAttribute var unreadCount: Int = 0 + @EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null @@ -42,7 +42,7 @@ abstract class RoomCategoryItem : VectorEpoxyModel() { val expandedArrowDrawable = ContextCompat.getDrawable(holder.rootView.context, expandedArrowDrawableRes)?.also { DrawableCompat.setTint(it, tintColor) } - holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadCount, showHighlighted)) + holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) holder.titleView.setCompoundDrawablesWithIntrinsicBounds(expandedArrowDrawable, null, null, null) holder.titleView.text = title holder.rootView.setOnClickListener { listener?.invoke() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt index 7c559b7f882..2536b969ff3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt @@ -27,7 +27,8 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.Displa return false } return when (displayMode) { - RoomListFragment.DisplayMode.HOME -> roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE + RoomListFragment.DisplayMode.HOME -> + roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE || roomSummary.userDrafts.isNotEmpty() RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN RoomListFragment.DisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt index 03bedbc7b59..42e3a3db85b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt @@ -101,7 +101,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri id(titleRes) title(stringProvider.getString(titleRes).toUpperCase()) expanded(isExpanded) - unreadCount(unreadCount) + unreadNotificationCount(unreadCount) showHighlighted(showHighlighted) listener { mutateExpandedState() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt index 72f5b973fbb..f5b62e45128 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt @@ -16,9 +16,11 @@ package im.vector.riotx.features.home.room.list +import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -36,7 +38,9 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var lastFormattedEvent: CharSequence @EpoxyAttribute lateinit var lastEventTime: CharSequence @EpoxyAttribute var avatarUrl: String? = null - @EpoxyAttribute var unreadCount: Int = 0 + @EpoxyAttribute var unreadNotificationCount: Int = 0 + @EpoxyAttribute var hasUnreadMessage: Boolean = false + @EpoxyAttribute var hasDraft: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null @@ -47,14 +51,18 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { holder.titleView.text = roomName holder.lastEventTimeView.text = lastEventTime holder.lastEventView.text = lastFormattedEvent - holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadCount, showHighlighted)) + holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) + holder.unreadIndentIndicator.isVisible = hasUnreadMessage + holder.draftView.isVisible = hasDraft avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) } class Holder : VectorEpoxyHolder() { val titleView by bind(R.id.roomNameView) val unreadCounterBadgeView by bind(R.id.roomUnreadCounterBadgeView) + val unreadIndentIndicator by bind(R.id.roomUnreadIndicator) val lastEventView by bind(R.id.roomLastEventView) + val draftView by bind(R.id.roomDraftBadge) val lastEventTimeView by bind(R.id.roomLastEventTimeView) val avatarImageView by bind(R.id.roomAvatarImageView) val rootView by bind(R.id.itemRoomLayout) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index 38f15974f30..942796961be 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.riotx.R +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider @@ -29,7 +30,6 @@ import im.vector.riotx.core.resources.DateProvider import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.home.room.detail.timeline.helper.senderName import me.gujun.android.span.span import javax.inject.Inject @@ -59,9 +59,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte rejectingErrorRoomsIds: Set, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { val secondLine = if (roomSummary.isDirect) { - roomSummary.latestEvent?.root?.senderId + roomSummary.latestPreviewableEvent?.root?.senderId } else { - roomSummary.latestEvent?.root?.senderId?.let { + roomSummary.latestPreviewableEvent?.root?.senderId?.let { stringProvider.getString(R.string.invited_by, it) } } @@ -88,13 +88,13 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte var latestFormattedEvent: CharSequence = "" var latestEventTime: CharSequence = "" - val latestEvent = roomSummary.latestEvent + val latestEvent = roomSummary.latestPreviewableEvent if (latestEvent != null) { val date = latestEvent.root.localDateTime() val currentDate = DateProvider.currentLocalDateTime() val isSameDay = date.toLocalDate() == currentDate.toLocalDate() latestFormattedEvent = if (latestEvent.root.isEncrypted() - && latestEvent.root.mxDecryptionResult == null) { + && latestEvent.root.mxDecryptionResult == null) { stringProvider.getString(R.string.encrypted_message) } else if (latestEvent.root.getClearType() == EventType.MESSAGE) { val senderName = latestEvent.senderName() ?: latestEvent.root.senderId @@ -131,7 +131,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte .roomName(roomSummary.displayName) .avatarUrl(roomSummary.avatarUrl) .showHighlighted(showHighlighted) - .unreadCount(unreadCount) + .unreadNotificationCount(unreadCount) + .hasUnreadMessage(roomSummary.hasUnreadMessages) + .hasDraft(roomSummary.userDrafts.isNotEmpty()) .listener { listener?.onRoomSelected(roomSummary) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/widget/FabMenuView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/widget/FabMenuView.kt index e95a0237adc..9f1f6526c9c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/widget/FabMenuView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/widget/FabMenuView.kt @@ -63,6 +63,18 @@ class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSe } } + override fun transitionToEnd() { + super.transitionToEnd() + + createRoomButton.contentDescription = context.getString(R.string.a11y_create_menu_close) + } + + override fun transitionToStart() { + super.transitionToStart() + + createRoomButton.contentDescription = context.getString(R.string.a11y_create_menu_open) + } + fun show() { isVisible = true createRoomButton.show() diff --git a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt index 476a70e7a4f..fcd2c011ee0 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/EventHtmlRenderer.kt @@ -40,7 +40,7 @@ import javax.inject.Singleton @Singleton class EventHtmlRenderer @Inject constructor(context: Context, - val avatarRenderer: AvatarRenderer, + avatarRenderer: AvatarRenderer, sessionHolder: ActiveSessionHolder) { private val markwon = Markwon.builder(context) .usePlugin(MatrixPlugin.create(GlideApp.with(context), context, avatarRenderer, sessionHolder)) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 6e559bcbe00..3c477c8a906 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.login import android.os.Bundle import android.view.View +import android.view.inputmethod.EditorInfo import android.widget.Toast import androidx.core.view.isVisible import androidx.transition.TransitionManager @@ -68,12 +69,19 @@ class LoginFragment : VectorBaseFragment() { homeServerField.focusChanges() .subscribe { if (!it) { - // TODO Also when clicking on button? viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString())) } } .disposeOnDestroy() + homeServerField.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString())) + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + val initHsUrl = viewModel.getInitialHomeServerUrl() if (initHsUrl != null) { homeServerField.setText(initHsUrl) @@ -170,6 +178,10 @@ class LoginFragment : VectorBaseFragment() { passwordContainer.isVisible = true authenticateButton.isVisible = true authenticateButtonSso.isVisible = false + if (loginField.text.isNullOrBlank() && passwordField.text.isNullOrBlank()) { + //Jump focus to login + loginField.requestFocus() + } } LoginMode.Sso -> { loginField.isVisible = false diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 72310893795..2367e1be189 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -137,16 +137,23 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } - private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) { - currentTask?.cancel() + private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) = withState { state -> + var newConfig : HomeServerConnectionConfig? = null Try { val homeServerUri = action.homeServerUrl - homeServerConnectionConfig = HomeServerConnectionConfig.Builder() + newConfig = HomeServerConnectionConfig.Builder() .withHomeServerUri(homeServerUri) .build() } + //Do not retry if we already have flows for this config -> causes infinite focus loop + if (newConfig?.homeServerUri?.toString() == homeServerConnectionConfig?.homeServerUri?.toString() + && state.asyncHomeServerLoginFlowRequest is Success) return@withState + + currentTask?.cancel() + homeServerConnectionConfig = newConfig + val homeServerConnectionConfigFinal = homeServerConnectionConfig if (homeServerConnectionConfigFinal == null) { diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt index 7a7c880ca70..55e7891f3c2 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt @@ -32,13 +32,14 @@ import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideRequest -import im.vector.riotx.core.utils.DimensionUtils.dpToPx +import im.vector.riotx.core.utils.DimensionConverter import kotlinx.android.parcel.Parcelize import timber.log.Timber import java.io.File import javax.inject.Inject -class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) { +class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, + private val dimensionConverter: DimensionConverter) { @Parcelize data class Data( @@ -67,10 +68,12 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: val (width, height) = processSize(data, mode) imageView.layoutParams.height = height imageView.layoutParams.width = width + // a11y + imageView.contentDescription = data.filename createGlideRequest(data, mode, imageView, width, height) .dontAnimate() - .transform(RoundedCorners(dpToPx(8, imageView.context))) + .transform(RoundedCorners(dimensionConverter.dpToPx(8))) .thumbnail(0.3f) .into(imageView) @@ -79,6 +82,9 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) { val (width, height) = processSize(data, mode) + // a11y + imageView.contentDescription = data.filename + createGlideRequest(data, mode, imageView, width, height) .listener(object : RequestListener { override fun onLoadFailed(e: GlideException?, @@ -126,6 +132,9 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: } fun render(data: Data, imageView: BigImageView) { + // a11y + imageView.contentDescription = data.filename + val (width, height) = processSize(data, Mode.THUMBNAIL) val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val fullSize = contentUrlResolver.resolveFullSize(data.url) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/BitmapLoader.kt b/vector/src/main/java/im/vector/riotx/features/notifications/BitmapLoader.kt index c3a74bd9c30..fbef196c530 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/BitmapLoader.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/BitmapLoader.kt @@ -26,7 +26,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class BitmapLoader @Inject constructor(val context: Context) { +class BitmapLoader @Inject constructor(private val context: Context) { /** * Avatar Url -> Bitmap diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/IconLoader.kt b/vector/src/main/java/im/vector/riotx/features/notifications/IconLoader.kt index 0c4477f41f0..dccae422793 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/IconLoader.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/IconLoader.kt @@ -28,7 +28,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class IconLoader @Inject constructor(val context: Context) { +class IconLoader @Inject constructor(private val context: Context) { /** * Avatar Url -> IconCompat diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt index d6f3655f4a7..5ecc66ffd4a 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/InviteNotifiableEvent.kt @@ -21,6 +21,7 @@ import androidx.core.app.NotificationCompat data class InviteNotifiableEvent( override var matrixID: String?, override val eventId: String, + override val editedEventId: String?, var roomId: String, override var noisy: Boolean, override val title: String, @@ -31,6 +32,7 @@ data class InviteNotifiableEvent( override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { override var hasBeenDisplayed: Boolean = false + override var isRedacted: Boolean = false override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt index ea2e21ab02b..1603ea5f001 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEvent.kt @@ -20,6 +20,7 @@ import java.io.Serializable interface NotifiableEvent : Serializable { var matrixID: String? val eventId: String + val editedEventId: String? var noisy: Boolean val title: String val description: String? @@ -30,6 +31,7 @@ interface NotifiableEvent : Serializable { // Compat: Only for android <7, for newer version the sound is defined in the channel var soundName: String? var hasBeenDisplayed: Boolean + var isRedacted: Boolean //Used to know if event should be replaced with the one coming from eventstream var isPushGatewayEvent: Boolean } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index 6650bf0d0d8..297e0b31e47 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt @@ -25,7 +25,8 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember 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.getEditedEventId +import im.vector.matrix.android.api.session.room.timeline.getLastMessageBody import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.riotx.BuildConfig import im.vector.riotx.R @@ -72,6 +73,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St return SimpleNotifiableEvent( session.myUserId, eventId = event.eventId!!, + editedEventId = timelineEvent.getEditedEventId(), noisy = false,//will be updated timestamp = event.originServerTs ?: System.currentTimeMillis(), description = bodyPreview, @@ -82,7 +84,6 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } } - private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? { //The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) @@ -93,14 +94,14 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St Timber.e("## Unable to resolve room for eventId [${event}]") // Ok room is not known in store, but we can still display something val body = - event.getLastMessageContent() - ?.body + event.getLastMessageBody() ?: stringProvider.getString(R.string.notification_unknown_new_event) val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val senderDisplayName = event.senderName ?: event.root.senderId val notifiableEvent = NotifiableMessageEvent( eventId = event.root.eventId!!, + editedEventId = event.getEditedEventId(), timestamp = event.root.originServerTs ?: 0, noisy = false,//will be updated senderName = senderDisplayName, @@ -128,14 +129,14 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } } - val body = event.getLastMessageContent() - ?.body + val body = event.getLastMessageBody() ?: stringProvider.getString(R.string.notification_unknown_new_event) val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.senderName ?: event.root.senderId val notifiableEvent = NotifiableMessageEvent( eventId = event.root.eventId!!, + editedEventId = event.getEditedEventId(), timestamp = event.root.originServerTs ?: 0, noisy = false,//will be updated senderName = senderDisplayName, @@ -177,6 +178,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St return InviteNotifiableEvent( session.myUserId, eventId = event.eventId!!, + editedEventId = null, roomId = roomId, timestamp = event.originServerTs ?: 0, noisy = false,//will be set later diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt index 11c2d744f63..5a3e989f18b 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableMessageEvent.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.EventType data class NotifiableMessageEvent( override val eventId: String, + override val editedEventId: String?, override var noisy: Boolean, override val timestamp: Long, var senderName: String?, @@ -35,6 +36,7 @@ data class NotifiableMessageEvent( override var soundName: String? = null override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC override var hasBeenDisplayed: Boolean = false + override var isRedacted: Boolean = false var roomAvatarPath: String? = null var senderAvatarPath: String? = null diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationAction.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationAction.kt index 869a204cf60..6322ecf998e 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationAction.kt @@ -19,23 +19,21 @@ import im.vector.matrix.android.api.pushrules.Action data class NotificationAction( val shouldNotify: Boolean, - val highlight: Boolean = false, - val soundName: String? = null -) { - companion object { - fun extractFrom(ruleActions: List): NotificationAction { - var shouldNotify = false - var highlight = false - var sound: String? = null - ruleActions.forEach { - if (it.type == Action.Type.NOTIFY) shouldNotify = true - if (it.type == Action.Type.DONT_NOTIFY) shouldNotify = false - if (it.type == Action.Type.SET_TWEAK) { - if (it.tweak_action == "highlight") highlight = it.boolValue ?: false - if (it.tweak_action == "sound") sound = it.stringValue - } - } - return NotificationAction(shouldNotify, highlight, sound) + val highlight: Boolean, + val soundName: String? +) + +fun List.toNotificationAction(): NotificationAction { + var shouldNotify = false + var highlight = false + var sound: String? = null + forEach { action -> + when (action) { + is Action.Notify -> shouldNotify = true + is Action.DoNotNotify -> shouldNotify = false + is Action.Highlight -> highlight = action.highlight + is Action.Sound -> sound = action.sound } } -} \ No newline at end of file + return NotificationAction(shouldNotify, highlight, sound) +} diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt index 638cf7d161e..cb68d0101d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt @@ -119,6 +119,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { val notifiableMessageEvent = NotifiableMessageEvent( // Generate a Fake event id UUID.randomUUID().toString(), + null, false, System.currentTimeMillis(), session.getUser(session.myUserId)?.displayName diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 551bbb808b3..13a39b5828c 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -15,7 +15,6 @@ */ package im.vector.riotx.features.notifications -import android.app.Notification import android.content.Context import android.graphics.Bitmap import android.os.Handler @@ -27,6 +26,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.settings.VectorPreferences import me.gujun.android.span.span import timber.log.Timber @@ -43,7 +43,9 @@ import javax.inject.Singleton */ @Singleton class NotificationDrawerManager @Inject constructor(private val context: Context, + private val notificationUtils: NotificationUtils, private val vectorPreferences: VectorPreferences, + private val stringProvider: StringProvider, private val activeSessionHolder: ActiveSessionHolder, private val iconLoader: IconLoader, private val bitmapLoader: BitmapLoader, @@ -96,12 +98,41 @@ class NotificationDrawerManager @Inject constructor(private val context: Context notifiableEvent.noisy = false eventList.remove(existing) eventList.add(notifiableEvent) - } else { //keep the existing one, do not replace } } else { - eventList.add(notifiableEvent) + // Check if this is an edit + if (notifiableEvent.editedEventId != null) { + // This is an edition + val eventBeforeEdition = eventList.firstOrNull { + // Edition of an event + it.eventId == notifiableEvent.editedEventId + // or edition of an edition + || it.editedEventId == notifiableEvent.editedEventId + } + + if (eventBeforeEdition != null) { + // Replace the existing notification with the new content + eventList.remove(eventBeforeEdition) + + eventList.add(notifiableEvent) + } else { + // Ignore an edit of a not displayed event in the notification drawer + } + } else { + // Not an edit + eventList.add(notifiableEvent) + } + } + } + } + + fun onEventRedacted(eventId: String) { + synchronized(eventList) { + eventList.filter { it.eventId == eventId }.map { notifiableEvent -> + notifiableEvent.isRedacted = true + notifiableEvent.hasBeenDisplayed = false } } } @@ -126,7 +157,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context e is NotifiableMessageEvent && e.roomId == roomId } } - NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID) + notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) } refreshNotificationDrawer() } @@ -194,20 +225,15 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val summaryInboxStyle = NotificationCompat.InboxStyle() //group events by room to create a single MessagingStyle notif - val roomIdToEventMap: MutableMap> = HashMap() - val simpleEvents: ArrayList = ArrayList() - val notifications: ArrayList = ArrayList() + val roomIdToEventMap: MutableMap> = LinkedHashMap() + val simpleEvents: MutableList = ArrayList() val eventIterator = eventList.listIterator() while (eventIterator.hasNext()) { val event = eventIterator.next() if (event is NotifiableMessageEvent) { val roomId = event.roomId - var roomEvents = roomIdToEventMap[roomId] - if (roomEvents == null) { - roomEvents = ArrayList() - roomIdToEventMap[roomId] = roomEvents - } + val roomEvents = roomIdToEventMap.getOrPut(roomId) { ArrayList() } if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) { //forget this event @@ -225,13 +251,13 @@ class NotificationDrawerManager @Inject constructor(private val context: Context var globalLastMessageTimestamp = 0L - //events have been grouped + //events have been grouped by roomId for ((roomId, events) in roomIdToEventMap) { - - if (events.isEmpty()) { + // Build the notification for the room + if (events.isEmpty() || events.all { it.isRedacted }) { //Just clear this notification Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events") - NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID) + notificationUtils.cancelNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID) continue } @@ -259,7 +285,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context for (event in events) { //if all events in this room have already been displayed there is no need to update it - if (!event.hasBeenDisplayed) { + if (!event.hasBeenDisplayed && !event.isRedacted) { roomEventGroupInfo.shouldBing = roomEventGroupInfo.shouldBing || event.noisy roomEventGroupInfo.customSound = event.soundName } @@ -272,16 +298,18 @@ class NotificationDrawerManager @Inject constructor(private val context: Context .build() if (event.outGoingMessage && event.outGoingMessageFailed) { - style.addMessage(context.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) + style.addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson) roomEventGroupInfo.hasSmartReplyError = true } else { - style.addMessage(event.body, event.timestamp, senderPerson) + if (!event.isRedacted) { + style.addMessage(event.body, event.timestamp, senderPerson) + } } event.hasBeenDisplayed = true //we can consider it as displayed //It is possible that this event was previously shown as an 'anonymous' simple notif. //And now it will be merged in a single MessageStyle notif, so we can clean to be sure - NotificationUtils.cancelNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID) + notificationUtils.cancelNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID) } try { @@ -307,7 +335,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context summaryInboxStyle.addLine(line) } } else { - val summaryLine = context.resources.getQuantityString( + val summaryLine = stringProvider.getQuantityString( R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size) summaryInboxStyle.addLine(summaryLine) } @@ -326,18 +354,16 @@ class NotificationDrawerManager @Inject constructor(private val context: Context globalLastMessageTimestamp = lastMessageTimestamp } - NotificationUtils.buildMessagesListNotification(context, - vectorPreferences, + val notification = notificationUtils.buildMessagesListNotification( style, roomEventGroupInfo, largeBitmap, lastMessageTimestamp, myUserDisplayName) - ?.let { - //is there an id for this room? - notifications.add(it) - NotificationUtils.showNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID, it) - } + + //is there an id for this room? + notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) + hasNewEvent = true summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing } else { @@ -350,14 +376,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context for (event in simpleEvents) { //We build a simple event if (firstTime || !event.hasBeenDisplayed) { - NotificationUtils.buildSimpleEventNotification(context, vectorPreferences, event, null, session.myUserId)?.let { - notifications.add(it) - NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it) - event.hasBeenDisplayed = true //we can consider it as displayed - hasNewEvent = true - summaryIsNoisy = summaryIsNoisy || event.noisy - summaryInboxStyle.addLine(event.description) - } + val notification = notificationUtils.buildSimpleEventNotification(event, null, session.myUserId) + notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification) + event.hasBeenDisplayed = true //we can consider it as displayed + hasNewEvent = true + summaryIsNoisy = summaryIsNoisy || event.noisy + summaryInboxStyle.addLine(event.description) } } @@ -375,28 +399,22 @@ class NotificationDrawerManager @Inject constructor(private val context: Context // To ensure the best experience on all devices and versions, always include a group summary when you create a group // https://developer.android.com/training/notify-user/group - if (eventList.isEmpty()) { - NotificationUtils.cancelNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID) + if (eventList.isEmpty() || eventList.all { it.isRedacted }) { + notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) } else { val nbEvents = roomIdToEventMap.size + simpleEvents.size - val sumTitle = context.resources.getQuantityString( - R.plurals.notification_compat_summary_title, nbEvents, nbEvents) + val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) summaryInboxStyle.setBigContentTitle(sumTitle) //TODO get latest event? - .setSummaryText( - context.resources - .getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) + .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) - NotificationUtils.buildSummaryListNotification( - context, - vectorPreferences, + val notification = notificationUtils.buildSummaryListNotification( summaryInboxStyle, sumTitle, noisy = hasNewEvent && summaryIsNoisy, - lastMessageTimestamp = globalLastMessageTimestamp - )?.let { - NotificationUtils.showNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID, it) - } + lastMessageTimestamp = globalLastMessageTimestamp) + + notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) if (hasNewEvent && summaryIsNoisy) { try { @@ -422,12 +440,11 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } } - private fun getRoomBitmap(events: ArrayList): Bitmap? { + private fun getRoomBitmap(events: List): Bitmap? { if (events.isEmpty()) return null //Use the last event (most recent?) - val roomAvatarPath = events.last().roomAvatarPath - ?: events.last().senderAvatarPath + val roomAvatarPath = events.last().roomAvatarPath ?: events.last().senderAvatarPath return bitmapLoader.getRoomBitmap(roomAvatarPath) } @@ -440,7 +457,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context fun persistInfo() { synchronized(eventList) { if (eventList.isEmpty()) { - deleteCachedRoomNotifications(context) + deleteCachedRoomNotifications() return } try { @@ -455,14 +472,14 @@ class NotificationDrawerManager @Inject constructor(private val context: Context } } - private fun loadEventInfo(): ArrayList { + private fun loadEventInfo(): MutableList { try { val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (file.exists()) { FileInputStream(file).use { val events: ArrayList? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE) if (events != null) { - return ArrayList(events.mapNotNull { it as? NotifiableEvent }) + return events.toMutableList() } } } @@ -472,7 +489,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context return ArrayList() } - private fun deleteCachedRoomNotifications(context: Context) { + private fun deleteCachedRoomNotifications() { val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (file.exists()) { file.delete() diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt index 4d7a3edfd80..2a4566d02c4 100755 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt @@ -27,7 +27,6 @@ import android.content.Intent import android.graphics.Bitmap import android.net.Uri import android.os.Build -import android.text.TextUtils import androidx.annotation.StringRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -37,6 +36,7 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import im.vector.riotx.BuildConfig import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.startNotificationChannelSettingsIntent import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity @@ -44,51 +44,73 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.settings.VectorPreferences import timber.log.Timber import java.util.* +import javax.inject.Inject +import javax.inject.Singleton -fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - /** * Util class for creating notifications. + * Note: Cannot inject ColorProvider in the constructor, because it requires an Activity */ -object NotificationUtils { +@Singleton +class NotificationUtils @Inject constructor(private val context: Context, + private val stringProvider: StringProvider, + private val vectorPreferences: VectorPreferences) { - /* ========================================================================================== - * IDs for notifications - * ========================================================================================== */ + companion object { + /* ========================================================================================== + * IDs for notifications + * ========================================================================================== */ - /** - * Identifier of the foreground notification used to keep the application alive - * when it runs in background. - * This notification, which is not removable by the end user, displays what - * the application is doing while in background. - */ - const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61 + /** + * Identifier of the foreground notification used to keep the application alive + * when it runs in background. + * This notification, which is not removable by the end user, displays what + * the application is doing while in background. + */ + const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61 - /* ========================================================================================== - * IDs for actions - * ========================================================================================== */ + /* ========================================================================================== + * IDs for actions + * ========================================================================================== */ - const val JOIN_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.JOIN_ACTION" - const val REJECT_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.REJECT_ACTION" - private const val QUICK_LAUNCH_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.QUICK_LAUNCH_ACTION" - const val MARK_ROOM_READ_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.MARK_ROOM_READ_ACTION" - const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION" - const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION" - const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION" - private const val TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION" + const val JOIN_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.JOIN_ACTION" + const val REJECT_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.REJECT_ACTION" + private const val QUICK_LAUNCH_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.QUICK_LAUNCH_ACTION" + const val MARK_ROOM_READ_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.MARK_ROOM_READ_ACTION" + const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION" + const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION" + const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION" + private const val TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION" - /* ========================================================================================== - * IDs for channels - * ========================================================================================== */ + /* ========================================================================================== + * IDs for channels + * ========================================================================================== */ + + // on devices >= android O, we need to define a channel for each notifications + private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" - // on devices >= android O, we need to define a channel for each notifications - private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID" + private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" - private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID" + private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" + private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" - private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" - private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2" + fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + + fun openSystemSettingsForSilentCategory(fragment: Fragment) { + startNotificationChannelSettingsIntent(fragment, SILENT_NOTIFICATION_CHANNEL_ID) + } + + fun openSystemSettingsForNoisyCategory(fragment: Fragment) { + startNotificationChannelSettingsIntent(fragment, NOISY_NOTIFICATION_CHANNEL_ID) + } + + fun openSystemSettingsForCallCategory(fragment: Fragment) { + startNotificationChannelSettingsIntent(fragment, CALL_NOTIFICATION_CHANNEL_ID) + } + } + + private val notificationManager = NotificationManagerCompat.from(context) /* ========================================================================================== * Channel names @@ -96,17 +118,13 @@ object NotificationUtils { /** * Create notification channels. - * - * @param context the context */ @TargetApi(Build.VERSION_CODES.O) - fun createNotificationChannels(context: Context) { + fun createNotificationChannels() { if (!supportNotificationChannels()) { return } - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) //Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE @@ -132,10 +150,10 @@ object NotificationUtils { * intrude. */ notificationManager.createNotificationChannel(NotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID, - context.getString(R.string.notification_noisy_notifications), + stringProvider.getString(R.string.notification_noisy_notifications), NotificationManager.IMPORTANCE_DEFAULT) .apply { - description = context.getString(R.string.notification_noisy_notifications) + description = stringProvider.getString(R.string.notification_noisy_notifications) enableVibration(true) enableLights(true) lightColor = accentColor @@ -145,29 +163,29 @@ object NotificationUtils { * Low notification importance: shows everywhere, but is not intrusive. */ notificationManager.createNotificationChannel(NotificationChannel(SILENT_NOTIFICATION_CHANNEL_ID, - context.getString(R.string.notification_silent_notifications), + stringProvider.getString(R.string.notification_silent_notifications), NotificationManager.IMPORTANCE_LOW) .apply { - description = context.getString(R.string.notification_silent_notifications) + description = stringProvider.getString(R.string.notification_silent_notifications) setSound(null, null) enableLights(true) lightColor = accentColor }) notificationManager.createNotificationChannel(NotificationChannel(LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, - context.getString(R.string.notification_listening_for_events), + stringProvider.getString(R.string.notification_listening_for_events), NotificationManager.IMPORTANCE_MIN) .apply { - description = context.getString(R.string.notification_listening_for_events) + description = stringProvider.getString(R.string.notification_listening_for_events) setSound(null, null) setShowBadge(false) }) notificationManager.createNotificationChannel(NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID, - context.getString(R.string.call), + stringProvider.getString(R.string.call), NotificationManager.IMPORTANCE_HIGH) .apply { - description = context.getString(R.string.call) + description = stringProvider.getString(R.string.call) setSound(null, null) enableLights(true) lightColor = accentColor @@ -177,12 +195,11 @@ object NotificationUtils { /** * Build a polling thread listener notification * - * @param context Android context * @param subTitleResId subtitle string resource Id of the notification * @return the polling thread listener notification */ @SuppressLint("NewApi") - fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int, withProgress: Boolean = true): Notification { + fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification { // build the pending intent go to the home screen if this is clicked. val i = Intent(context, HomeActivity::class.java) i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP @@ -191,7 +208,7 @@ object NotificationUtils { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val builder = NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID) - .setContentTitle(context.getString(subTitleResId)) + .setContentTitle(stringProvider.getString(subTitleResId)) .setSmallIcon(R.drawable.sync) .setCategory(NotificationCompat.CATEGORY_SERVICE) .setColor(accentColor) @@ -225,7 +242,7 @@ object NotificationUtils { CharSequence::class.java, CharSequence::class.java, PendingIntent::class.java) - deprecatedMethod.invoke(notification, context, context.getString(R.string.app_name), context.getString(subTitleResId), pi) + deprecatedMethod.invoke(notification, context, stringProvider.getString(R.string.app_name), stringProvider.getString(subTitleResId), pi) } catch (ex: Exception) { Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=") } @@ -238,7 +255,6 @@ object NotificationUtils { * Build an incoming call notification. * This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow. * - * @param context the context. * @param isVideo true if this is a video call, false for voice call * @param roomName the room name in which the call is pending. * @param matrixId the matrix id @@ -246,20 +262,19 @@ object NotificationUtils { * @return the call notification. */ @SuppressLint("NewApi") - fun buildIncomingCallNotification(context: Context, - isVideo: Boolean, + fun buildIncomingCallNotification(isVideo: Boolean, roomName: String, matrixId: String, callId: String): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) - .setContentTitle(ensureTitleNotEmpty(context, roomName)) + .setContentTitle(ensureTitleNotEmpty(roomName)) .apply { if (isVideo) { - setContentText(context.getString(R.string.incoming_video_call)) + setContentText(stringProvider.getString(R.string.incoming_video_call)) } else { - setContentText(context.getString(R.string.incoming_voice_call)) + setContentText(stringProvider.getString(R.string.incoming_voice_call)) } } .setSmallIcon(R.drawable.incoming_call_notification_transparent) @@ -295,7 +310,6 @@ object NotificationUtils { /** * Build a pending call notification * - * @param context the context. * @param isVideo true if this is a video call, false for voice call * @param roomName the room name in which the call is pending. * @param roomId the room Id @@ -304,20 +318,19 @@ object NotificationUtils { * @return the call notification. */ @SuppressLint("NewApi") - fun buildPendingCallNotification(context: Context, - isVideo: Boolean, + fun buildPendingCallNotification(isVideo: Boolean, roomName: String, roomId: String, matrixId: String, callId: String): Notification { val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) - .setContentTitle(ensureTitleNotEmpty(context, roomName)) + .setContentTitle(ensureTitleNotEmpty(roomName)) .apply { if (isVideo) { - setContentText(context.getString(R.string.video_call_in_progress)) + setContentText(stringProvider.getString(R.string.video_call_in_progress)) } else { - setContentText(context.getString(R.string.call_in_progress)) + setContentText(stringProvider.getString(R.string.call_in_progress)) } } .setSmallIcon(R.drawable.incoming_call_notification_transparent) @@ -355,9 +368,9 @@ object NotificationUtils { /** * Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended */ - fun buildCallEndedNotification(context: Context): Notification { + fun buildCallEndedNotification(): Notification { return NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) - .setContentTitle(context.getString(R.string.call_ended)) + .setContentTitle(stringProvider.getString(R.string.call_ended)) .setSmallIcon(R.drawable.ic_material_call_end_grey) .setCategory(NotificationCompat.CATEGORY_CALL) .build() @@ -366,17 +379,14 @@ object NotificationUtils { /** * Build a notification for a Room */ - fun buildMessagesListNotification(context: Context, - vectorPreferences: VectorPreferences, - messageStyle: NotificationCompat.MessagingStyle, + fun buildMessagesListNotification(messageStyle: NotificationCompat.MessagingStyle, roomInfo: RoomEventGroupInfo, largeIcon: Bitmap?, lastMessageTimestamp: Long, - senderDisplayNameForReplyCompat: String?): Notification? { - + senderDisplayNameForReplyCompat: String?): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked - val openRoomIntent = buildOpenRoomIntent(context, roomInfo.roomId) + val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId) val smallIcon = R.drawable.ic_status_bar val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID @@ -393,18 +403,15 @@ object NotificationUtils { // Title for API < 16 devices. .setContentTitle(roomInfo.roomDisplayName) // Content for API < 16 devices. - .setContentText(context.getString(R.string.notification_new_messages)) + .setContentText(stringProvider.getString(R.string.notification_new_messages)) // Number of new notifications for API <24 (M and below) devices. - .setSubText(context - .resources - .getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size) - ) + .setSubText(stringProvider.getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size)) // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) // devices and all Wear devices. But we want a custom grouping, so we specify the groupID // TODO Group should be current user display name - .setGroup(context.getString(R.string.app_name)) + .setGroup(stringProvider.getString(R.string.app_name)) //In order to avoid notification making sound twice (due to the summary notification) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) @@ -440,17 +447,17 @@ object NotificationUtils { addAction(NotificationCompat.Action( R.drawable.ic_material_done_all_white, - context.getString(R.string.action_mark_room_read), + stringProvider.getString(R.string.action_mark_room_read), markRoomReadPendingIntent)) // Quick reply if (!roomInfo.hasSmartReplyError) { - buildQuickReplyIntent(context, roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> + buildQuickReplyIntent(roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) - .setLabel(context.getString(R.string.action_quick_reply)) + .setLabel(stringProvider.getString(R.string.action_quick_reply)) .build() NotificationCompat.Action.Builder(R.drawable.vector_notification_quick_reply, - context.getString(R.string.action_quick_reply), replyPendingIntent) + stringProvider.getString(R.string.action_quick_reply), replyPendingIntent) .addRemoteInput(remoteInput) .build()?.let { addAction(it) @@ -477,11 +484,9 @@ object NotificationUtils { } - fun buildSimpleEventNotification(context: Context, - vectorPreferences: VectorPreferences, - simpleNotifiableEvent: NotifiableEvent, + fun buildSimpleEventNotification(simpleNotifiableEvent: NotifiableEvent, largeIcon: Bitmap?, - matrixId: String): Notification? { + matrixId: String): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked val smallIcon = R.drawable.ic_status_bar @@ -489,9 +494,9 @@ object NotificationUtils { val channelID = if (simpleNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID return NotificationCompat.Builder(context, channelID) - .setContentTitle(context.getString(R.string.app_name)) + .setContentTitle(stringProvider.getString(R.string.app_name)) .setContentText(simpleNotifiableEvent.description) - .setGroup(context.getString(R.string.app_name)) + .setGroup(stringProvider.getString(R.string.app_name)) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) .setSmallIcon(smallIcon) .setColor(accentColor) @@ -508,7 +513,7 @@ object NotificationUtils { addAction( R.drawable.vector_notification_reject_invitation, - context.getString(R.string.reject), + stringProvider.getString(R.string.reject), rejectIntentPendingIntent) // offer to type a quick accept button @@ -520,7 +525,7 @@ object NotificationUtils { PendingIntent.FLAG_UPDATE_CURRENT) addAction( R.drawable.vector_notification_accept_invitation, - context.getString(R.string.join), + stringProvider.getString(R.string.join), joinIntentPendingIntent) } else { setAutoCancel(true) @@ -551,7 +556,7 @@ object NotificationUtils { .build() } - private fun buildOpenRoomIntent(context: Context, roomId: String): PendingIntent? { + private fun buildOpenRoomIntent(roomId: String): PendingIntent? { val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId)) roomIntentTap.action = TAP_TO_VIEW_ACTION //pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that @@ -564,7 +569,7 @@ object NotificationUtils { .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) } - private fun buildOpenHomePendingIntentForSummary(context: Context): PendingIntent { + private fun buildOpenHomePendingIntentForSummary(): PendingIntent { val intent = HomeActivity.newIntent(context, clearNotification = true) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP intent.data = Uri.parse("foobar://tapSummary") @@ -578,7 +583,7 @@ object NotificationUtils { However, for Android devices running Marshmallow and below (API level 23 and below), it will be more appropriate to use an activity. Since you have to provide your own UI. */ - private fun buildQuickReplyIntent(context: Context, roomId: String, senderName: String?): PendingIntent? { + private fun buildQuickReplyIntent(roomId: String, senderName: String?): PendingIntent? { val intent: Intent if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent = Intent(context, NotificationBroadcastReceiver::class.java) @@ -610,12 +615,10 @@ object NotificationUtils { /** * Build the summary notification */ - fun buildSummaryListNotification(context: Context, - vectorPreferences: VectorPreferences, - style: NotificationCompat.InboxStyle, + fun buildSummaryListNotification(style: NotificationCompat.InboxStyle, compatSummary: String, noisy: Boolean, - lastMessageTimestamp: Long): Notification? { + lastMessageTimestamp: Long): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val smallIcon = R.drawable.ic_status_bar @@ -623,12 +626,12 @@ object NotificationUtils { // used in compat < N, after summary is built based on child notifications .setWhen(lastMessageTimestamp) .setStyle(style) - .setContentTitle(context.getString(R.string.app_name)) + .setContentTitle(stringProvider.getString(R.string.app_name)) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setSmallIcon(smallIcon) //set content text to support devices running API level < 24 .setContentText(compatSummary) - .setGroup(context.getString(R.string.app_name)) + .setGroup(stringProvider.getString(R.string.app_name)) //set this notification as the summary for the group .setGroupSummary(true) .setColor(accentColor) @@ -645,13 +648,12 @@ object NotificationUtils { priority = NotificationCompat.PRIORITY_LOW } } - .setContentIntent(buildOpenHomePendingIntentForSummary(context)) - .setDeleteIntent(getDismissSummaryPendingIntent(context)) + .setContentIntent(buildOpenHomePendingIntentForSummary()) + .setDeleteIntent(getDismissSummaryPendingIntent()) .build() - } - private fun getDismissSummaryPendingIntent(context: Context): PendingIntent { + private fun getDismissSummaryPendingIntent(): PendingIntent { val intent = Intent(context, NotificationBroadcastReceiver::class.java) intent.action = DISMISS_SUMMARY_ACTION intent.data = Uri.parse("foobar://deleteSummary") @@ -659,33 +661,28 @@ object NotificationUtils { 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } - fun showNotificationMessage(context: Context, tag: String?, id: Int, notification: Notification) { - with(NotificationManagerCompat.from(context)) { - notify(tag, id, notification) - } + fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { + notificationManager.notify(tag, id, notification) } - fun cancelNotificationMessage(context: Context, tag: String?, id: Int) { - NotificationManagerCompat.from(context) - .cancel(tag, id) + fun cancelNotificationMessage(tag: String?, id: Int) { + notificationManager.cancel(tag, id) } /** * Cancel the foreground notification service */ - fun cancelNotificationForegroundService(context: Context) { - NotificationManagerCompat.from(context) - .cancel(NOTIFICATION_ID_FOREGROUND_SERVICE) + fun cancelNotificationForegroundService() { + notificationManager.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE) } /** * Cancel all the notification */ - fun cancelAllNotifications(context: Context) { + fun cancelAllNotifications() { // Keep this try catch (reported by GA) try { - NotificationManagerCompat.from(context) - .cancelAll() + notificationManager.cancelAll() } catch (e: Exception) { Timber.e(e, "## cancelAllNotifications() failed " + e.message) } @@ -694,7 +691,7 @@ object NotificationUtils { /** * Return true it the user has enabled the do not disturb mode */ - fun isDoNotDisturbModeOn(context: Context): Boolean { + fun isDoNotDisturbModeOn(): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return false } @@ -706,23 +703,11 @@ object NotificationUtils { || setting == NotificationManager.INTERRUPTION_FILTER_ALARMS } - private fun ensureTitleNotEmpty(context: Context, title: String?): CharSequence { - if (TextUtils.isEmpty(title)) { - return context.getString(R.string.app_name) + private fun ensureTitleNotEmpty(title: String?): CharSequence { + if (title.isNullOrBlank()) { + return stringProvider.getString(R.string.app_name) } - return title!! - } - - fun openSystemSettingsForSilentCategory(fragment: Fragment) { - startNotificationChannelSettingsIntent(fragment, SILENT_NOTIFICATION_CHANNEL_ID) - } - - fun openSystemSettingsForNoisyCategory(fragment: Fragment) { - startNotificationChannelSettingsIntent(fragment, NOISY_NOTIFICATION_CHANNEL_ID) - } - - fun openSystemSettingsForCallCategory(fragment: Fragment) { - startNotificationChannelSettingsIntent(fragment, CALL_NOTIFICATION_CHANNEL_ID) + return title } } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt index 555c8737c58..7f1c501334e 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt @@ -40,7 +40,7 @@ class PushRuleTriggerListener @Inject constructor( Timber.e("Called without active session") return } - val notificationAction = NotificationAction.extractFrom(actions) + val notificationAction = actions.toNotificationAction() if (notificationAction.shouldNotify) { val notifiableEvent = resolver.resolveEvent(event, session!!) if (notifiableEvent == null) { @@ -60,6 +60,10 @@ class PushRuleTriggerListener @Inject constructor( notificationDrawerManager.clearMessageEventOfRoom(roomId) } + override fun onEventRedacted(redactedEventId: String) { + notificationDrawerManager.onEventRedacted(redactedEventId) + } + override fun batchFinish() { notificationDrawerManager.refreshNotificationDrawer() } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt b/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt index 8ec5ff9580b..66779cfe9a5 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/SimpleNotifiableEvent.kt @@ -20,6 +20,7 @@ import androidx.core.app.NotificationCompat data class SimpleNotifiableEvent( override var matrixID: String?, override val eventId: String, + override val editedEventId: String?, override var noisy: Boolean, override val title: String, override val description: String, @@ -29,6 +30,7 @@ data class SimpleNotifiableEvent( override var isPushGatewayEvent: Boolean = false) : NotifiableEvent { override var hasBeenDisplayed: Boolean = false + override var isRedacted: Boolean = false override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt index 47a7722e9d3..9502370219c 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsController.kt @@ -33,7 +33,7 @@ import javax.inject.Inject class PublicRoomsController @Inject constructor(private val stringProvider: StringProvider, private val avatarRenderer: AvatarRenderer, - private val errorFormatter: ErrorFormatter) : TypedEpoxyController() { + private val errorFormatter: ErrorFormatter) : TypedEpoxyController() { var callback: Callback? = null diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt index 32cd6732731..2dfef5536fd 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt @@ -143,10 +143,15 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback viewModel.loadMore() } + var initialValueSet = false + override fun invalidate() = withState(viewModel) { state -> - if (publicRoomsFilter.query.toString() != state.currentFilter) { - // For initial filter - publicRoomsFilter.setQuery(state.currentFilter, false) + if (!initialValueSet) { + initialValueSet = true + if (publicRoomsFilter.query.toString() != state.currentFilter) { + // For initial filter + publicRoomsFilter.setQuery(state.currentFilter, false) + } } // Populate list with Epoxy diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index bf36fb265d3..456ea238342 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.* 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.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom @@ -176,6 +177,11 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: } override fun onFailure(failure: Throwable) { + if (failure is Failure.Cancelled) { + // Ignore, another request should be already started + return + } + currentTask = null setState { @@ -220,4 +226,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: }) } + override fun onCleared() { + super.onCleared() + + currentTask?.cancel() + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt index ad58c0ce26f..7559606315a 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomController.kt @@ -31,7 +31,7 @@ import im.vector.riotx.features.form.formSwitchItem import javax.inject.Inject class CreateRoomController @Inject constructor(private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter + private val errorFormatter: ErrorFormatter ) : TypedEpoxyController() { var listener: Listener? = null diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt index cdd489a9ed0..ec70a126ccb 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedNotificationPreferenceFragment.kt @@ -29,7 +29,6 @@ import im.vector.riotx.core.extensions.withArgs import im.vector.riotx.core.preference.BingRule import im.vector.riotx.core.preference.BingRulePreference import im.vector.riotx.features.notifications.NotificationUtils -import im.vector.riotx.features.notifications.supportNotificationChannels import javax.inject.Inject class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseFragment() { @@ -56,7 +55,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF override fun bindPref() { val callNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY) - if (supportNotificationChannels()) { + if (NotificationUtils.supportNotificationChannels()) { callNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { NotificationUtils.openSystemSettingsForCallCategory(this) false @@ -66,7 +65,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF } val noisyNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY) - if (supportNotificationChannels()) { + if (NotificationUtils.supportNotificationChannels()) { noisyNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { NotificationUtils.openSystemSettingsForNoisyCategory(this) false @@ -76,7 +75,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF } val silentNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY) - if (supportNotificationChannels()) { + if (NotificationUtils.supportNotificationChannels()) { silentNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener { NotificationUtils.openSystemSettingsForSilentCategory(this) false @@ -89,7 +88,7 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF // Ringtone val ringtonePreference = findPreference(VectorPreferences.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY) - if (supportNotificationChannels()) { + if (NotificationUtils.supportNotificationChannels()) { ringtonePreference.isVisible = false } else { ringtonePreference.summary = vectorPreferences.getNotificationRingToneName() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationFragment.kt index 6536c170eef..691618a1518 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationFragment.kt @@ -21,6 +21,7 @@ import androidx.preference.Preference import androidx.preference.SwitchPreference import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.pushrules.RuleIds +import im.vector.matrix.android.api.pushrules.RuleKind import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent @@ -45,12 +46,13 @@ class VectorSettingsNotificationPreferenceFragment : VectorSettingsBaseFragment( .find { it.ruleId == RuleIds.RULE_ID_DISABLE_ALL } if (mRuleMaster == null) { + // The home server does not support RULE_ID_DISABLE_ALL, so hide the preference pref.isVisible = false return } - val areNotifEnabledAtAccountLevelt = !mRuleMaster.enabled - (pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevelt + val areNotifEnabledAtAccountLevel = !mRuleMaster.enabled + (pref as SwitchPreference).isChecked = areNotifEnabledAtAccountLevel } } @@ -114,19 +116,21 @@ class VectorSettingsNotificationPreferenceFragment : VectorSettingsBaseFragment( .find { it.ruleId == RuleIds.RULE_ID_DISABLE_ALL } ?.let { //Trick, we must enable this room to disable notifications - pushRuleService.updatePushRuleEnableStatus("override", it, !switchPref.isChecked, - object : MatrixCallback { - - override fun onSuccess(data: Unit) { - pushRuleService.fetchPushRules() - } - - override fun onFailure(failure: Throwable) { - //revert the check box - switchPref.isChecked = !switchPref.isChecked - Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show() - } - }) + pushRuleService.updatePushRuleEnableStatus(RuleKind.OVERRIDE, + it, + !switchPref.isChecked, + object : MatrixCallback { + + override fun onSuccess(data: Unit) { + // Push rules will be updated form the sync + } + + override fun onFailure(failure: Throwable) { + //revert the check box + switchPref.isChecked = !switchPref.isChecked + Toast.makeText(activity, R.string.unknown_error, Toast.LENGTH_SHORT).show() + } + }) } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/push/PushRuleItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/push/PushRuleItem.kt index 47c9a06a09d..5e38a7f7ca9 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/push/PushRuleItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/push/PushRuleItem.kt @@ -26,11 +26,11 @@ import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelWithHolder -import im.vector.matrix.android.api.pushrules.Action +import im.vector.matrix.android.api.pushrules.getActions import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder -import im.vector.riotx.features.notifications.NotificationAction +import im.vector.riotx.features.notifications.toNotificationAction @EpoxyModelClass(layout = R.layout.item_pushrule_raw) @@ -50,12 +50,12 @@ abstract class PushRuleItem : EpoxyModelWithHolder() { holder.view.setBackgroundColor(ContextCompat.getColor(context, R.color.vector_silver_color)) holder.ruleId.text = "[Disabled] ${pushRule.ruleId}" } - val actions = Action.mapFrom(pushRule) - if (actions.isNullOrEmpty()) { + val actions = pushRule.getActions() + if (actions.isEmpty()) { holder.actionIcon.isInvisible = true } else { holder.actionIcon.isVisible = true - val notifAction = NotificationAction.extractFrom(actions) + val notifAction = actions.toNotificationAction() if (notifAction.shouldNotify && !notifAction.soundName.isNullOrBlank()) { holder.actionIcon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_action_notify_noisy)) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestAccountSettings.kt b/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestAccountSettings.kt index 47d1f6358a7..24c9dc1c1a3 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestAccountSettings.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestAccountSettings.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.settings.troubleshoot import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.pushrules.RuleIds +import im.vector.matrix.android.api.pushrules.RuleKind import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.resources.StringProvider @@ -45,8 +46,7 @@ class TestAccountSettings @Inject constructor(private val stringProvider: String override fun doFix() { if (manager?.diagStatus == TestStatus.RUNNING) return //wait before all is finished - // TODO Use constant for kind - session.updatePushRuleEnableStatus("override", defaultRule, !defaultRule.enabled, + session.updatePushRuleEnableStatus(RuleKind.OVERRIDE, defaultRule, !defaultRule.enabled, object : MatrixCallback { override fun onSuccess(data: Unit) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestBingRulesSettings.kt b/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestBingRulesSettings.kt index 8f353df2827..a58d48061d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestBingRulesSettings.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/troubleshoot/TestBingRulesSettings.kt @@ -15,12 +15,12 @@ */ package im.vector.riotx.features.settings.troubleshoot -import im.vector.matrix.android.api.pushrules.Action import im.vector.matrix.android.api.pushrules.RuleIds +import im.vector.matrix.android.api.pushrules.getActions import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.notifications.NotificationAction +import im.vector.riotx.features.notifications.toNotificationAction import javax.inject.Inject class TestBingRulesSettings @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, @@ -29,15 +29,15 @@ class TestBingRulesSettings @Inject constructor(private val activeSessionHolder: private val testedRules = listOf(RuleIds.RULE_ID_CONTAIN_DISPLAY_NAME, - RuleIds.RULE_ID_CONTAIN_USER_NAME, - RuleIds.RULE_ID_ONE_TO_ONE_ROOM, - RuleIds.RULE_ID_ALL_OTHER_MESSAGES_ROOMS) + RuleIds.RULE_ID_CONTAIN_USER_NAME, + RuleIds.RULE_ID_ONE_TO_ONE_ROOM, + RuleIds.RULE_ID_ALL_OTHER_MESSAGES_ROOMS) val ruleSettingsName = arrayOf(R.string.settings_containing_my_display_name, - R.string.settings_containing_my_user_name, - R.string.settings_messages_in_one_to_one, - R.string.settings_messages_in_group_chat) + R.string.settings_containing_my_user_name, + R.string.settings_messages_in_one_to_one, + R.string.settings_messages_in_group_chat) override fun perform() { val session = activeSessionHolder.getSafeActiveSession() ?: return @@ -50,8 +50,8 @@ class TestBingRulesSettings @Inject constructor(private val activeSessionHolder: var oneOrMoreRuleAreSilent = false for ((index, ruleId) in testedRules.withIndex()) { pushRules.find { it.ruleId == ruleId }?.let { rule -> - val actions = Action.mapFrom(rule) ?: return@let - val notifAction = NotificationAction.extractFrom(actions) + val actions = rule.getActions() + val notifAction = actions.toNotificationAction() if (!rule.enabled || !notifAction.shouldNotify) { //off oneOrMoreRuleIsOff = true diff --git a/vector/src/main/res/layout/fragment_home_detail.xml b/vector/src/main/res/layout/fragment_home_detail.xml index 8bc0603013c..bda23973ee9 100644 --- a/vector/src/main/res/layout/fragment_home_detail.xml +++ b/vector/src/main/res/layout/fragment_home_detail.xml @@ -25,6 +25,7 @@ android:id="@+id/groupToolbarAvatarImageView" android:layout_width="32dp" android:layout_height="32dp" + android:contentDescription="@string/a11y_open_drawer" tools:src="@tools:sample/avatars" /> diff --git a/vector/src/main/res/layout/fragment_room_list.xml b/vector/src/main/res/layout/fragment_room_list.xml index 0259ca1ea25..30ab86d0040 100644 --- a/vector/src/main/res/layout/fragment_room_list.xml +++ b/vector/src/main/res/layout/fragment_room_list.xml @@ -30,6 +30,8 @@ android:layout_marginEnd="16dp" android:layout_marginRight="16dp" android:layout_marginBottom="16dp" + android:accessibilityTraversalBefore="@+id/roomListEpoxyRecyclerView" + android:contentDescription="@string/a11y_create_direct_message" android:scaleType="center" android:src="@drawable/ic_fab_add_chat" android:visibility="gone" @@ -45,6 +47,8 @@ android:layout_marginEnd="16dp" android:layout_marginRight="16dp" android:layout_marginBottom="16dp" + android:accessibilityTraversalBefore="@+id/roomListEpoxyRecyclerView" + android:contentDescription="@string/a11y_create_room" android:src="@drawable/ic_fab_add_room" android:visibility="gone" app:maxImageSize="32dp" diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml index b110e200824..741bd47069a 100644 --- a/vector/src/main/res/layout/item_room.xml +++ b/vector/src/main/res/layout/item_room.xml @@ -10,12 +10,22 @@ android:focusable="true" android:foreground="?attr/selectableItemBackground"> + + + + + tools:text="4" + tools:visibility="visible" /> @@ -88,6 +89,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:background="?android:attr/selectableItemBackground" + android:contentDescription="@string/send_attachment" android:src="@drawable/ic_attachment" android:tint="?attr/colorAccent" tools:ignore="MissingConstraints" /> @@ -107,6 +109,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:background="?android:attr/selectableItemBackground" + android:contentDescription="@string/send" android:src="@drawable/ic_send" android:tint="?attr/colorAccent" tools:ignore="MissingConstraints" /> diff --git a/vector/src/main/res/layout/motion_fab_menu_merge.xml b/vector/src/main/res/layout/motion_fab_menu_merge.xml index a7f82fe4b10..02ba4341c61 100644 --- a/vector/src/main/res/layout/motion_fab_menu_merge.xml +++ b/vector/src/main/res/layout/motion_fab_menu_merge.xml @@ -13,7 +13,10 @@ android:id="@+id/createRoomTouchGuard" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?riotx_touch_guard_bg" /> + android:background="?riotx_touch_guard_bg" + android:clickable="true" + android:contentDescription="@string/a11y_create_menu_close" + android:focusable="true" /> @@ -43,6 +49,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" + android:accessibilityTraversalBefore="@+id/createRoomItemGroup" + android:contentDescription="@string/a11y_create_direct_message" android:src="@drawable/ic_fab_add_chat" app:backgroundTint="#FFFFFF" app:fabCustomSize="48dp" @@ -57,6 +65,7 @@ android:layout_marginEnd="8dp" android:layout_marginRight="8dp" android:ellipsize="end" + android:importantForAccessibility="no" android:text="@string/fab_menu_create_chat" /> @@ -64,6 +73,8 @@ android:id="@+id/createRoomButton" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:accessibilityTraversalBefore="@+id/createRoomItemChat" + android:contentDescription="@string/a11y_create_menu_open" android:src="@drawable/ic_fab_add" app:maxImageSize="14dp" /> diff --git a/vector/src/main/res/layout/view_keys_backup_banner.xml b/vector/src/main/res/layout/view_keys_backup_banner.xml index d0738262614..87c92cf8b4e 100644 --- a/vector/src/main/res/layout/view_keys_backup_banner.xml +++ b/vector/src/main/res/layout/view_keys_backup_banner.xml @@ -106,6 +106,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" + android:contentDescription="@string/a11y_close_keys_backup_banner" android:src="@drawable/ic_small_close" app:layout_constraintEnd_toEndOf="@id/view_keys_backup_banner_close" app:layout_constraintStart_toStartOf="@id/view_keys_backup_banner_close" diff --git a/vector/src/main/res/menu/home_bottom_navigation.xml b/vector/src/main/res/menu/home_bottom_navigation.xml index 407e57d50a1..f93a018436d 100644 --- a/vector/src/main/res/menu/home_bottom_navigation.xml +++ b/vector/src/main/res/menu/home_bottom_navigation.xml @@ -3,23 +3,20 @@ + android:title="@string/bottom_action_home" /> + android:title="@string/bottom_action_people_x" /> + android:title="@string/bottom_action_rooms" /> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index e02de698067..6b46d359beb 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -11,5 +11,15 @@ It looks like you’re trying to connect to another homeserver. Do you want to sign out? + Looks like the server is taking to long to respond, this can be caused by either poor connectivity or an error with our servers. Please try again in a while. + + Send attachment + + Open the navigation drawer + Open the create room menu + Close the create room menu… + Create a new direct conversation + Create a new room + Close keys backup banner \ No newline at end of file