diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index cf1cd5b9ff..de434d0122 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -3,14 +3,36 @@ # 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 - steps: - - label: "Assemble GPlay Debug version" + - label: "Compile and run Unit tests" agents: # We use a medium sized instance instead of the normal small ones because - # gradle build is long + # gradle build can be memory hungry queue: "medium" + commands: + - "./gradlew clean test --stacktrace" + plugins: + - docker#v3.1.0: + image: "runmymind/docker-android-sdk" + propagate-environment: true + + - label: "Compile Android tests" + agents: + # We use a medium sized instance instead of the normal small ones because + # gradle build can be memory hungry + queue: "medium" + commands: + - "./gradlew clean assembleAndroidTest --stacktrace" + plugins: + - docker#v3.1.0: + image: "runmymind/docker-android-sdk" + propagate-environment: true + + - label: "Assemble GPlay Debug version" + agents: + # We use a xlarge sized instance instead of the normal small ones because + # gradle build can be memory hungry + queue: "xlarge" commands: - "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace" artifact_paths: @@ -23,9 +45,9 @@ steps: - label: "Assemble FDroid Debug version" agents: - # We use a medium sized instance instead of the normal small ones because - # gradle build is long - queue: "medium" + # We use a xlarge sized instance instead of the normal small ones because + # gradle build can be memory hungry + queue: "xlarge" commands: - "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace" artifact_paths: @@ -38,9 +60,9 @@ steps: - label: "Build Google Play unsigned APK" agents: - # We use a medium sized instance instead of the normal small ones because - # gradle build is long - queue: "medium" + # We use a xlarge sized instance instead of the normal small ones because + # gradle build can be memory hungry + queue: "xlarge" commands: - "./gradlew clean assembleGplayRelease --stacktrace" artifact_paths: diff --git a/CHANGES.md b/CHANGES.md index 48d0be34cd..33ba41e778 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,26 @@ +Changes in RiotX 0.8.0 (2019-11-19) +=================================================== + +Features ✨: + - Handle long click on room in the room list (#395) + - Ignore/UnIgnore users, and display list of ignored users (#542, #617) + +Improvements 🙌: + - Search reaction by name or keyword in emoji picker + - Handle code tags (#567) + - Support spoiler messages + - Support m.sticker and m.room.join_rules events in timeline + +Other changes: + - Markdown set to off by default (#412) + - Accessibility improvements to the attachment file type chooser + +Bugfix 🐛: + - Fix issues with some member events rendering (#498) + - Passphrase does not match (Export room keys) (#644) + - Ask for permission to write external storage when uri comes from the keyboard (#658) + - Fix issue with english US/GB translation (#671) + Changes in RiotX 0.7.0 (2019-10-24) =================================================== @@ -12,6 +35,7 @@ Improvements: - Attachments: start using system pickers (#52) - Mark all messages as read (#396) + Other changes: - Accessibility improvements to read receipts in the room timeline and reactions emoji chooser diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d64dd7110e..45834afa21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,6 +86,10 @@ Also, if possible, please test your change on a real device. Testing on Android When adding new string resources, please only add new entries in file `value/strings.xml`. Translations will be added later by the community of translators with a specific tool named [Weblate](https://translate.riot.im/projects/riot-android/). Do not hesitate to use plurals when appropriate. +### Accessibility + +Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`. + ### Layout When adding or editing layouts, make sure the layout will render correctly if device uses a RTL (Right To Left) language. diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 31f928c241..1d8e81e44f 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -11,6 +11,8 @@ android { versionCode 1 versionName "1.0" + // Multidex is useful for tests + multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/matrix-sdk-android-rx/src/androidTest/java/im/vector/matrix/rx/ExampleInstrumentedTest.java b/matrix-sdk-android-rx/src/androidTest/java/im/vector/matrix/rx/ExampleInstrumentedTest.java deleted file mode 100644 index 986d40d1a9..0000000000 --- a/matrix-sdk-android-rx/src/androidTest/java/im/vector/matrix/rx/ExampleInstrumentedTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.rx; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("im.vector.matrix.rx.test", appContext.getPackageName()); - } -} 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 e058b2716c..6793d6249d 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.notification.RoomNotificationState import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Optional @@ -67,6 +68,10 @@ class RxRoom(private val room: Room) { fun liveDrafts(): Observable> { return room.getDraftsLive().asObservable() } + + fun liveNotificationState(): Observable { + return room.getLiveRoomNotificationState().asObservable() + } } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index f19777b6f5..1572851d3a 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -54,6 +54,10 @@ class RxSession(private val session: Session) { return session.liveUsers().asObservable() } + fun liveIgnoredUsers(): Observable> { + return session.liveIgnoredUsers().asObservable() + } + fun livePagedUsers(filter: String? = null): Observable> { return session.livePagedUsers(filter).asObservable() } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 3e6d3ea88b..ab5f122dbc 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -155,7 +155,8 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation 'org.robolectric:robolectric:4.3' //testImplementation 'org.robolectric:shadows-support-v4:3.0' - testImplementation 'io.mockk:mockk:1.9.3.kotlin12' + // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 + testImplementation 'io.mockk:mockk:1.9.2.kotlin12' testImplementation 'org.amshove.kluent:kluent-android:1.44' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" @@ -165,7 +166,8 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' - androidTestImplementation 'io.mockk:mockk-android:1.9.3.kotlin12' + // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 + androidTestImplementation 'io.mockk:mockk-android:1.9.2.kotlin12' androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt index 3cd47d4998..99fe7d29b4 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt @@ -17,12 +17,12 @@ package im.vector.matrix.android import android.content.Context -import androidx.test.InstrumentationRegistry +import androidx.test.core.app.ApplicationProvider import java.io.File interface InstrumentedTest { fun context(): Context { - return InstrumentationRegistry.getTargetContext() + return ApplicationProvider.getApplicationContext() } fun cacheDir(): File { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt index 7d33fae4d8..5c86f5ad22 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt @@ -17,8 +17,8 @@ package im.vector.matrix.android.auth import androidx.test.annotation.UiThreadTest +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.GrantPermissionRule -import androidx.test.runner.AndroidJUnit4 import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.OkReplayRuleChainNoActivity import im.vector.matrix.android.api.auth.Authenticator diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt index 77a3cde249..c1dfa465fb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt @@ -67,5 +67,5 @@ interface Authenticator { /** * Create a session after a SSO successful login */ - fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session + fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/Emojis.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/Emojis.kt new file mode 100644 index 0000000000..943b2c1b10 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/Emojis.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.api.crypto + +import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation +import im.vector.matrix.android.internal.crypto.verification.getEmojiForCode + +/** + * Provide all the emojis used for SAS verification (for debug purpose) + */ +fun getAllVerificationEmojis(): List { + return (0..63).map { getEmojiForCode(it) } +} 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 d135504055..a81af2cf21 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 @@ -21,7 +21,7 @@ import timber.log.Timber sealed class Action { object Notify : Action() object DoNotNotify : Action() - data class Sound(val sound: String) : Action() + data class Sound(val sound: String = ACTION_OBJECT_VALUE_VALUE_DEFAULT) : Action() data class Highlight(val highlight: Boolean) : Action() } @@ -63,6 +63,29 @@ private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default" * * */ + +@Suppress("IMPLICIT_CAST_TO_ANY") +fun List.toJson(): List { + return map { action -> + when (action) { + is Action.Notify -> ACTION_NOTIFY + is Action.DoNotNotify -> ACTION_DONT_NOTIFY + is Action.Sound -> { + mapOf( + ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_SOUND, + ACTION_OBJECT_VALUE_KEY to action.sound + ) + } + is Action.Highlight -> { + mapOf( + ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT, + ACTION_OBJECT_VALUE_KEY to action.highlight + ) + } + } + } +} + fun PushRule.getActions(): List { val result = ArrayList() 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 aa277ea8bd..0ef70eb99b 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 @@ -34,6 +34,10 @@ interface PushRuleService { fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback): Cancelable + fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable + + fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable + fun addPushRuleListener(listener: PushRuleListener) fun removePushRuleListener(listener: PushRuleListener) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt index f1e4ca6c7b..a84e5af48c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/cache/CacheService.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.cache import im.vector.matrix.android.api.MatrixCallback /** - * This interface defines a method to sign out. It's implemented at the session level. + * This interface defines a method to clear the cache. It's implemented at the session level. */ interface CacheService { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt index ca75871cda..1dbee475e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/LocalEcho.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.api.session.events.model -import java.util.* +import java.util.UUID object LocalEcho { 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 70c9c6e36c..90790a6ab0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.relation.RelationService +import im.vector.matrix.android.api.session.room.notification.RoomPushRuleService import im.vector.matrix.android.api.session.room.reporting.ReportingService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.DraftService @@ -41,7 +42,8 @@ interface Room : StateService, ReportingService, RelationService, - RoomCryptoService { + RoomCryptoService, + RoomPushRuleService { /** * The roomId of this room diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomJoinRules.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomJoinRules.kt new file mode 100644 index 0000000000..d7cf8678c3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomJoinRules.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.model + +import com.squareup.moshi.Json + +/** + * Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules + */ +enum class RoomJoinRules(val value: String) { + + @Json(name = "public") + PUBLIC("public"), + + @Json(name = "invite") + INVITE("invite"), + + @Json(name = "knock") + KNOCK("knock"), + + @Json(name = "private") + PRIVATE("private") +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomJoinRulesContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomJoinRulesContent.kt new file mode 100644 index 0000000000..cf6b182e2d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomJoinRulesContent.kt @@ -0,0 +1,29 @@ +/* + * 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.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the EventType.STATE_ROOM_JOIN_RULES state event content + */ +@JsonClass(generateAdapter = true) +data class RoomJoinRulesContent( + @Json(name = "join_rule") val joinRules: RoomJoinRules? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt index 107a8b276d..fbac261802 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt @@ -38,7 +38,7 @@ data class MessageImageContent( /** * Metadata about the image referred to in url. */ - @Json(name = "info") val info: ImageInfo? = null, + @Json(name = "info") override val info: ImageInfo? = null, /** * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image. @@ -52,4 +52,4 @@ data class MessageImageContent( * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null -) : MessageEncryptedContent +) : MessageImageInfoContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageInfoContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageInfoContent.kt new file mode 100644 index 0000000000..9087a45b4c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageInfoContent.kt @@ -0,0 +1,25 @@ +/* + * 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.model.message + +/** + * A content with image information + */ +interface MessageImageInfoContent : MessageEncryptedContent { + val info: ImageInfo? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt new file mode 100644 index 0000000000..d1b4a5c3cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageStickerContent.kt @@ -0,0 +1,56 @@ +/* + * 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.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo + +@JsonClass(generateAdapter = true) +data class MessageStickerContent( + /** + * Set in local, not from server + */ + override val type: String = MessageType.MSGTYPE_STICKER_LOCAL, + + /** + * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image, + * or some kind of content description for accessibility e.g. 'image attachment'. + */ + @Json(name = "body") override val body: String, + + /** + * Metadata about the image referred to in url. + */ + @Json(name = "info") override val info: ImageInfo? = null, + + /** + * Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image. + */ + @Json(name = "url") override val url: String? = null, + + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, + + /** + * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. + */ + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null +) : MessageImageInfoContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/notification/RoomNotificationState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/notification/RoomNotificationState.kt new file mode 100644 index 0000000000..a638b2710c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/notification/RoomNotificationState.kt @@ -0,0 +1,42 @@ +/* + * 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.notification + +/** + * Defines the room notification state + */ +enum class RoomNotificationState { + /** + * All the messages will trigger a noisy notification + */ + ALL_MESSAGES_NOISY, + + /** + * All the messages will trigger a notification + */ + ALL_MESSAGES, + + /** + * Only the messages with user display name / user name will trigger notifications + */ + MENTIONS_ONLY, + + /** + * No notifications + */ + MUTE +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/notification/RoomPushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/notification/RoomPushRuleService.kt new file mode 100644 index 0000000000..41cd484ef9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/notification/RoomPushRuleService.kt @@ -0,0 +1,28 @@ +/* + * 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.notification + +import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +interface RoomPushRuleService { + + fun getLiveRoomNotificationState(): LiveData + + fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback): Cancelable +} 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 43c1544ffd..ad747efee9 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 @@ -23,6 +23,7 @@ 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 import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent @@ -62,15 +63,11 @@ data class TimelineEvent( } fun getDisambiguatedDisplayName(): String { - return if (isUniqueDisplayName) { - senderName - } else { - senderName?.let { name -> - "$name (${root.senderId})" - } + return when { + senderName.isNullOrBlank() -> root.senderId ?: "" + isUniqueDisplayName -> senderName + else -> "$senderName (${root.senderId})" } - ?: root.senderId - ?: "" } /** @@ -103,8 +100,14 @@ fun TimelineEvent.getEditedEventId(): String? { /** * Get last MessageContent, after a possible edition */ -fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel() - ?: root.getClearContent().toModel() +fun TimelineEvent.getLastMessageContent(): MessageContent? { + return if (root.getClearType() == EventType.STICKER) { + root.getClearContent().toModel() + } else { + annotations?.editSummary?.aggregatedContent?.toModel() + ?: root.getClearContent().toModel() + } +} /** * Get last Message body, after a possible edition @@ -113,7 +116,8 @@ fun TimelineEvent.getLastMessageBody(): String? { val lastMessageContent = getLastMessageContent() if (lastMessageContent != null) { - return lastMessageContent.newContent?.toModel()?.body ?: lastMessageContent.body + return lastMessageContent.newContent?.toModel()?.body + ?: lastMessageContent.body } return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index d3de777e34..2a93a876f6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -64,4 +64,19 @@ interface UserService { * @return a Livedata of users */ fun livePagedUsers(filter: String? = null): LiveData> + + /** + * Get list of ignored users + */ + fun liveIgnoredUsers(): LiveData> + + /** + * Ignore users + */ + fun ignoreUserIds(userIds: List, callback: MatrixCallback): Cancelable + + /** + * Un-ignore some users + */ + fun unIgnoreUserIds(userIds: List, callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Types.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Types.kt index bfb9a59956..f83166512e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Types.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Types.kt @@ -21,4 +21,6 @@ import java.lang.reflect.ParameterizedType typealias JsonDict = Map +val emptyJsonDict = emptyMap() + internal val JSON_DICT_PARAMETERIZED_TYPE: ParameterizedType = Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index e379090677..ff49d4308b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.auth import android.util.Patterns +import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.data.Credentials @@ -39,10 +40,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import javax.inject.Inject -import javax.inject.Provider internal class DefaultAuthenticator @Inject constructor(@Unauthenticated - private val okHttpClient: Provider, + private val okHttpClient: Lazy, private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionParamsStore: SessionParamsStore, @@ -112,14 +112,27 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated sessionManager.getOrCreateSession(sessionParams) } - override fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session { + override fun createSessionFromSso(credentials: Credentials, + homeServerConnectionConfig: HomeServerConnectionConfig, + callback: MatrixCallback): Cancelable { + val job = GlobalScope.launch(coroutineDispatchers.main) { + val sessionOrFailure = runCatching { + createSessionFromSso(credentials, homeServerConnectionConfig) + } + sessionOrFailure.foldToCallback(callback) + } + return CancelableCoroutine(job) + } + + private suspend fun createSessionFromSso(credentials: Credentials, + homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { val sessionParams = SessionParams(credentials, homeServerConnectionConfig) sessionParamsStore.save(sessionParams) - return sessionManager.getOrCreateSession(sessionParams) + sessionManager.getOrCreateSession(sessionParams) } private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { - val retrofit = retrofitFactory.create(okHttpClient.get(), homeServerConnectionConfig.homeServerUri.toString()) + val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt index e1fef7e2eb..17bcb9dc81 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.auth -import arrow.core.Try import im.vector.matrix.android.api.auth.data.SessionParams internal interface SessionParamsStore { @@ -27,9 +26,9 @@ internal interface SessionParamsStore { fun getAll(): List - fun save(sessionParams: SessionParams): Try + suspend fun save(sessionParams: SessionParams) - fun delete(userId: String): Try + suspend fun delete(userId: String) - fun deleteAll(): Try + suspend fun deleteAll() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt index 7ec5d24559..00fde2682e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt @@ -16,9 +16,9 @@ package im.vector.matrix.android.internal.auth.db -import arrow.core.Try import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.di.AuthDatabase import io.realm.Realm import io.realm.RealmConfiguration @@ -62,41 +62,29 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S return sessionParams } - override fun save(sessionParams: SessionParams): Try { - return Try { + override suspend fun save(sessionParams: SessionParams) { + awaitTransaction(realmConfiguration) { val entity = mapper.map(sessionParams) if (entity != null) { - val realm = Realm.getInstance(realmConfiguration) - realm.executeTransaction { - it.insert(entity) - } - realm.close() + it.insert(entity) } } } - override fun delete(userId: String): Try { - return Try { - val realm = Realm.getInstance(realmConfiguration) - realm.executeTransaction { - it.where(SessionParamsEntity::class.java) - .equalTo(SessionParamsEntityFields.USER_ID, userId) - .findAll() - .deleteAllFromRealm() - } - realm.close() + override suspend fun delete(userId: String) { + awaitTransaction(realmConfiguration) { + it.where(SessionParamsEntity::class.java) + .equalTo(SessionParamsEntityFields.USER_ID, userId) + .findAll() + .deleteAllFromRealm() } } - override fun deleteAll(): Try { - return Try { - val realm = Realm.getInstance(realmConfiguration) - realm.executeTransaction { - it.where(SessionParamsEntity::class.java) - .findAll() - .deleteAllFromRealm() - } - realm.close() + override suspend fun deleteAll() { + awaitTransaction(realmConfiguration) { + it.where(SessionParamsEntity::class.java) + .findAll() + .deleteAllFromRealm() } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt index 7f2a23e4c2..b2002f0916 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt @@ -66,7 +66,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM if (':' in userId) { try { synchronized(notReadyToRetryHS) { - res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1)) + res = !notReadyToRetryHS.contains(userId.substringAfterLast(':')) } } catch (e: Exception) { Timber.e(e, "## canRetryKeysDownload() failed") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt index 89a27c9463..86e8a1825c 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt @@ -216,7 +216,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor( sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback { private fun onDone(state: OutgoingRoomKeyRequest.RequestState) { if (request.state !== OutgoingRoomKeyRequest.RequestState.UNSENT) { - Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to " + request.state) + Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to ${request.state}") } else { request.state = state cryptoStore.updateOutgoingRoomKeyRequest(request) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt index 5ef2b9b1a2..81988fe209 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt @@ -41,8 +41,7 @@ fun doWithRealm(realmConfiguration: RealmConfiguration, action: (Realm) -> T */ fun doRealmQueryAndCopy(realmConfiguration: RealmConfiguration, action: (Realm) -> T?): T? { return Realm.getInstance(realmConfiguration).use { realm -> - val result = action.invoke(realm) - result?.let { realm.copyFromRealm(it) } + action.invoke(realm)?.let { realm.copyFromRealm(it) } } } @@ -51,8 +50,7 @@ fun doRealmQueryAndCopy(realmConfiguration: RealmConfiguration */ fun doRealmQueryAndCopyList(realmConfiguration: RealmConfiguration, action: (Realm) -> Iterable): Iterable { return Realm.getInstance(realmConfiguration).use { realm -> - val result = action.invoke(realm) - realm.copyFromRealm(result) + action.invoke(realm).let { realm.copyFromRealm(it) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index d88a84de9e..8a3b66dfe5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -91,7 +91,7 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati realmLocker = Realm.getInstance(realmConfiguration) // Ensure CryptoMetadataEntity is inserted in DB - doWithRealm(realmConfiguration) { realm -> + doRealmTransaction(realmConfiguration) { realm -> var currentMetadata = realm.where().findFirst() var deleteAll = false @@ -109,15 +109,13 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati } if (currentMetadata == null) { - realm.executeTransaction { - if (deleteAll) { - it.deleteAll() - } + if (deleteAll) { + realm.deleteAll() + } - // Metadata not found, or database cleaned, create it - it.createObject(CryptoMetadataEntity::class.java, credentials.userId).apply { - deviceId = credentials.deviceId - } + // Metadata not found, or database cleaned, create it + realm.createObject(CryptoMetadataEntity::class.java, credentials.userId).apply { + deviceId = credentials.deviceId } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt index ca1157e583..e0cd47e0e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -43,6 +43,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber +import java.lang.Exception import java.util.UUID import javax.inject.Inject import kotlin.collections.HashMap @@ -166,72 +167,59 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre return } // Download device keys prior to everything - checkKeysAreDownloaded( - otherUserId!!, - startReq, - success = { - Timber.v("## SAS onStartRequestReceived ${startReq.transactionID!!}") - val tid = startReq.transactionID!! - val existing = getExistingTransaction(otherUserId, tid) - val existingTxs = getExistingTransactionsForUser(otherUserId) - if (existing != null) { - // should cancel both! - Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}") - existing.cancel(CancelCode.UnexpectedMessage) - cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) - } else if (existingTxs?.isEmpty() == false) { - Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}") - // Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time. - existingTxs.forEach { - it.cancel(CancelCode.UnexpectedMessage) - } - cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) - } else { - // Ok we can create - if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) { - Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") - val tx = IncomingSASVerificationTransaction( - this, - setDeviceVerificationAction, - credentials, - cryptoStore, - sendToDeviceTask, - taskExecutor, - myDeviceInfoHolder.get().myDevice.fingerprint()!!, - startReq.transactionID!!, - otherUserId) - addTransaction(tx) - tx.acceptToDeviceEvent(otherUserId, startReq) - } else { - Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}") - cancelTransaction(tid, otherUserId, startReq.fromDevice - ?: event.getSenderKey()!!, CancelCode.UnknownMethod) - } - } - }, - error = { - cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) - }) + if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) { + Timber.v("## SAS onStartRequestReceived ${startReq.transactionID!!}") + val tid = startReq.transactionID!! + val existing = getExistingTransaction(otherUserId, tid) + val existingTxs = getExistingTransactionsForUser(otherUserId) + if (existing != null) { + // should cancel both! + Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}") + existing.cancel(CancelCode.UnexpectedMessage) + cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) + } else if (existingTxs?.isEmpty() == false) { + Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}") + // Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time. + existingTxs.forEach { + it.cancel(CancelCode.UnexpectedMessage) + } + cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) + } else { + // Ok we can create + if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) { + Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") + val tx = IncomingSASVerificationTransaction( + this, + setDeviceVerificationAction, + credentials, + cryptoStore, + sendToDeviceTask, + taskExecutor, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + startReq.transactionID!!, + otherUserId) + addTransaction(tx) + tx.acceptToDeviceEvent(otherUserId, startReq) + } else { + Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}") + cancelTransaction(tid, otherUserId, startReq.fromDevice + ?: event.getSenderKey()!!, CancelCode.UnknownMethod) + } + } + } else { + cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) + } } private suspend fun checkKeysAreDownloaded(otherUserId: String, - startReq: KeyVerificationStart, - success: (MXUsersDevicesMap) -> Unit, - error: () -> Unit) { - runCatching { - deviceListManager.downloadKeys(listOf(otherUserId), true) - }.fold( - { - if (it.getUserDeviceIds(otherUserId)?.contains(startReq.fromDevice) == true) { - success(it) - } else { - error() - } - }, - { - error() - } - ) + startReq: KeyVerificationStart): MXUsersDevicesMap? { + return try { + val keys = deviceListManager.downloadKeys(listOf(otherUserId), true) + val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null + keys.takeIf { deviceIds.contains(startReq.fromDevice) } + } catch (e: Exception) { + null + } } private suspend fun onCancelReceived(event: Event) { @@ -342,10 +330,8 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre private fun addTransaction(tx: VerificationTransaction) { tx.otherUserId.let { otherUserId -> synchronized(txMap) { - if (txMap[otherUserId] == null) { - txMap[otherUserId] = HashMap() - } - txMap[otherUserId]?.set(tx.transactionId, tx) + val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() } + txInnerMap[tx.transactionId] = tx dispatchTxAdded(tx) tx.addListener(this) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/AsyncTransaction.kt index 36e68e5cf3..0f8445d20f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/AsyncTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/AsyncTransaction.kt @@ -20,14 +20,19 @@ import io.realm.RealmConfiguration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +import timber.log.Timber -suspend fun awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> Unit) = withContext(Dispatchers.IO) { +suspend fun awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> Unit) = withContext(Dispatchers.Default) { Realm.getInstance(config).use { bgRealm -> bgRealm.beginTransaction() try { + val start = System.currentTimeMillis() transaction(bgRealm) if (isActive) { bgRealm.commitTransaction() + val end = System.currentTimeMillis() + val time = end - start + Timber.v("Execute transaction in $time millis") } } finally { if (bgRealm.isInTransaction) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt index 24765c120d..36ed2f7edf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt @@ -39,14 +39,17 @@ internal fun TimelineEventEntity.updateSenderData() { val isUnlinked = chunkEntity.isUnlinked() var senderMembershipEvent: EventEntity? var senderRoomMemberContent: String? + var senderRoomMemberPrevContent: String? when { stateIndex <= 0 -> { senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root senderRoomMemberContent = senderMembershipEvent?.prevContent + senderRoomMemberPrevContent = senderMembershipEvent?.content } else -> { senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root senderRoomMemberContent = senderMembershipEvent?.content + senderRoomMemberPrevContent = senderMembershipEvent?.prevContent } } @@ -58,11 +61,27 @@ internal fun TimelineEventEntity.updateSenderData() { .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) .prev(since = stateIndex) senderRoomMemberContent = senderMembershipEvent?.content + senderRoomMemberPrevContent = senderMembershipEvent?.prevContent + } + + ContentMapper.map(senderRoomMemberContent).toModel()?.also { + this.senderAvatar = it.avatarUrl + this.senderName = it.displayName + this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) + } + + // We try to fallback on prev content if we got a room member state events with null fields + if (root?.type == EventType.STATE_ROOM_MEMBER) { + ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also { + if (this.senderAvatar == null && it.avatarUrl != null) { + this.senderAvatar = it.avatarUrl + } + if (this.senderName == null && it.displayName != null) { + this.senderName = it.displayName + this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) + } + } } - val senderRoomMember: RoomMember? = ContentMapper.map(senderRoomMemberContent).toModel() - this.senderAvatar = senderRoomMember?.avatarUrl - this.senderName = senderRoomMember?.displayName - this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(senderRoomMember?.displayName) this.senderMembershipEvent = senderMembershipEvent } 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 5db062b000..2577bec581 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,7 +22,7 @@ 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.* +import java.util.UUID import javax.inject.Inject internal class RoomSummaryMapper @Inject constructor( @@ -36,7 +36,7 @@ internal class RoomSummaryMapper @Inject constructor( } val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let { - timelineEventMapper.map(it) + timelineEventMapper.map(it, buildReadReceipts = false) } if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) { // TODO use a global event decryptor? attache to session and that listen to new sessionId? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/IgnoredUserEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/IgnoredUserEntity.kt new file mode 100644 index 0000000000..bd31046f82 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/IgnoredUserEntity.kt @@ -0,0 +1,24 @@ +/* + * 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 IgnoredUserEntity(var userId: String = "") : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRuleEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRuleEntity.kt index 4744c8d053..1ef65d9dea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRuleEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PushRuleEntity.kt @@ -17,6 +17,8 @@ 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 internal open class PushRuleEntity( // Required. The actions to perform when this rule is matched. @@ -33,5 +35,8 @@ internal open class PushRuleEntity( var pattern: String? = null ) : RealmObject() { + @LinkingObjects("pushRules") + val parent: RealmResults? = null + companion object } 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 21b2fdce5a..76b355b064 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 @@ -35,6 +35,7 @@ import io.realm.annotations.RealmModule RoomTagEntity::class, SyncEntity::class, UserEntity::class, + IgnoredUserEntity::class, EventAnnotationsSummaryEntity::class, ReactionAggregatedSummaryEntity::class, EditAggregatedSummaryEntity::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt index 8fbbb1311e..4f64f2896f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt @@ -16,27 +16,26 @@ package im.vector.matrix.android.internal.database.query +import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.model.FilterEntity import im.vector.matrix.android.internal.session.filter.FilterFactory import io.realm.Realm -import io.realm.kotlin.createObject import io.realm.kotlin.where /** * Get the current filter, create one if it does not exist */ -internal fun FilterEntity.Companion.getFilter(realm: Realm): FilterEntity { +internal suspend fun FilterEntity.Companion.getFilter(realm: Realm): FilterEntity { var filter = realm.where().findFirst() if (filter == null) { - realm.executeTransaction { - realm.createObject().apply { - filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString() - roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString() - filterId = "" - } + filter = FilterEntity().apply { + filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString() + roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString() + filterId = "" + } + awaitTransaction(realm.configuration) { + it.insert(filter) } - filter = realm.where().findFirst()!! } - return filter } 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 4ecb40a7e1..42e7770114 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 @@ -16,10 +16,10 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.api.pushrules.RuleKind +import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.model.PushRuleEntity 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 -import im.vector.matrix.android.internal.database.model.PusherEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where @@ -41,3 +41,11 @@ internal fun PushRulesEntity.Companion.where(realm: Realm, .equalTo(PushRulesEntityFields.SCOPE, scope) .equalTo(PushRulesEntityFields.KIND_STR, kind.name) } + +internal fun PushRuleEntity.Companion.where(realm: Realm, + scope: String, + ruleId: String): RealmQuery { + return realm.where() + .equalTo("${PushRuleEntityFields.PARENT}.${PushRulesEntityFields.SCOPE}", scope) + .equalTo(PushRuleEntityFields.RULE_ID, ruleId) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt index e0a507f939..6b996d1285 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt @@ -24,8 +24,7 @@ import io.realm.kotlin.where internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery { return realm.where() - .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId) - .equalTo(ReadReceiptEntityFields.USER_ID, userId) + .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId)) } internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery { @@ -45,8 +44,10 @@ internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { return ReadReceiptEntity.where(realm, roomId, userId).findFirst() - ?: realm.createObject(ReadReceiptEntity::class.java, "${roomId}_$userId").apply { + ?: realm.createObject(ReadReceiptEntity::class.java, buildPrimaryKey(roomId, userId)).apply { this.roomId = roomId this.userId = userId } } + +private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 49474e8e6b..3bd035c0b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -111,7 +111,7 @@ internal fun RealmQuery.prev(since: Int? = null, strict: Bo internal fun RealmList.find(eventId: String): TimelineEventEntity? { return this.where() - .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, eventId) + .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) .findFirst() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index 811950ac15..c17864b82b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -36,7 +36,7 @@ internal object MatrixModule { @MatrixScope fun providesMatrixCoroutineDispatchers(): MatrixCoroutineDispatchers { return MatrixCoroutineDispatchers(io = Dispatchers.IO, - computation = Dispatchers.IO, + computation = Dispatchers.Default, main = Dispatchers.Main, crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(), sync = Executors.newSingleThreadExecutor().asCoroutineDispatcher() 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 d8db462f7c..96cdf29226 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 @@ -20,10 +20,11 @@ import com.squareup.moshi.Moshi import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory 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 +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers +import im.vector.matrix.android.internal.session.sync.model.accountdata.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(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST) .registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES) ) .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) 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 bfc37d733d..3d850c223a 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 @@ -22,7 +22,7 @@ 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 java.util.Collections import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt index 15e6f76381..44ccd7c941 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt @@ -17,17 +17,24 @@ package im.vector.matrix.android.internal.network import com.squareup.moshi.Moshi +import dagger.Lazy +import okhttp3.Call import okhttp3.OkHttpClient +import okhttp3.Request import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import javax.inject.Inject class RetrofitFactory @Inject constructor(private val moshi: Moshi) { - fun create(okHttpClient: OkHttpClient, baseUrl: String): Retrofit { + fun create(okHttpClient: Lazy, baseUrl: String): Retrofit { return Retrofit.Builder() .baseUrl(baseUrl) - .client(okHttpClient) + .callFactory(object : Call.Factory { + override fun newCall(request: Request): Call { + return okHttpClient.get().newCall(request) + } + }) .addConverterFactory(UnitConverterFactory) .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() 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 d038630a74..0e88894969 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 @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session import android.content.Context import com.zhuinden.monarchy.Monarchy import dagger.Binds +import dagger.Lazy import dagger.Module import dagger.Provides import dagger.multibindings.IntoSet @@ -132,7 +133,7 @@ internal abstract class SessionModule { @JvmStatic @Provides @SessionScope - fun providesRetrofit(@Authenticated okHttpClient: OkHttpClient, + fun providesRetrofit(@Authenticated okHttpClient: Lazy, sessionParams: SessionParams, retrofitFactory: RetrofitFactory): Retrofit { return retrofitFactory diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt index 02e6db189a..53967784a1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt @@ -16,54 +16,44 @@ package im.vector.matrix.android.internal.session.filter +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.model.FilterEntity import im.vector.matrix.android.internal.database.model.FilterEntityFields import im.vector.matrix.android.internal.database.query.getFilter -import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm -import io.realm.RealmConfiguration import io.realm.kotlin.where import javax.inject.Inject -internal class DefaultFilterRepository @Inject constructor( - @SessionDatabase private val realmConfiguration: RealmConfiguration -) : FilterRepository { +internal class DefaultFilterRepository @Inject constructor(private val monarchy: Monarchy) : FilterRepository { - override fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean { - val result: Boolean + override suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val filter = FilterEntity.getFilter(realm) + val result = if (filter.filterBodyJson != filterBody.toJSONString()) { + // Filter has changed, store it and reset the filter Id + monarchy.awaitTransaction { + // We manage only one filter for now + val filterBodyJson = filterBody.toJSONString() + val roomEventFilterJson = roomEventFilter.toJSONString() - val realm = Realm.getInstance(realmConfiguration) + val filterEntity = FilterEntity.getFilter(it) - val filter = FilterEntity.getFilter(realm) - - if (filter.filterBodyJson != filterBody.toJSONString()) { - // Filter has changed, store it and reset the filter Id - realm.executeTransaction { - // We manage only one filter for now - val filterBodyJson = filterBody.toJSONString() - val roomEventFilterJson = roomEventFilter.toJSONString() - - val filterEntity = FilterEntity.getFilter(it) - - filterEntity.filterBodyJson = filterBodyJson - filterEntity.roomEventFilterJson = roomEventFilterJson - // Reset filterId - filterEntity.filterId = "" + filterEntity.filterBodyJson = filterBodyJson + filterEntity.roomEventFilterJson = roomEventFilterJson + // Reset filterId + filterEntity.filterId = "" + } + true + } else { + filter.filterId.isBlank() } - result = true - } else { - result = filter.filterId.isBlank() + result } - - realm.close() - - return result } - override fun storeFilterId(filterBody: FilterBody, filterId: String) { - val realm = Realm.getInstance(realmConfiguration) - - realm.executeTransaction { + override suspend fun storeFilterId(filterBody: FilterBody, filterId: String) { + monarchy.awaitTransaction { // We manage only one filter for now val filterBodyJson = filterBody.toJSONString() @@ -73,39 +63,24 @@ internal class DefaultFilterRepository @Inject constructor( ?.findFirst() ?.filterId = filterId } - - realm.close() } - override fun getFilter(): String { - val result: String - - val realm = Realm.getInstance(realmConfiguration) - - val filter = FilterEntity.getFilter(realm) - - result = if (filter.filterId.isBlank()) { - // Use the Json format - filter.filterBodyJson - } else { - // Use FilterId - filter.filterId + override suspend fun getFilter(): String { + return Realm.getInstance(monarchy.realmConfiguration).use { + val filter = FilterEntity.getFilter(it) + if (filter.filterId.isBlank()) { + // Use the Json format + filter.filterBodyJson + } else { + // Use FilterId + filter.filterId + } } - - realm.close() - - return result } - override fun getRoomFilter(): String { - val realm = Realm.getInstance(realmConfiguration) - - val filter = FilterEntity.getFilter(realm) - - val result = filter.roomEventFilterJson - - realm.close() - - return result + override suspend fun getRoomFilter(): String { + return Realm.getInstance(monarchy.realmConfiguration).use { + FilterEntity.getFilter(it).roomEventFilterJson + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterService.kt index 84e820ebca..c85d949d0a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterService.kt @@ -21,36 +21,13 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import javax.inject.Inject -internal class DefaultFilterService @Inject constructor(private val filterRepository: FilterRepository, - private val saveFilterTask: SaveFilterTask, +internal class DefaultFilterService @Inject constructor(private val saveFilterTask: SaveFilterTask, private val taskExecutor: TaskExecutor) : FilterService { // TODO Pass a list of support events instead override fun setFilter(filterPreset: FilterService.FilterPreset) { - val filterBody = when (filterPreset) { - FilterService.FilterPreset.RiotFilter -> { - FilterFactory.createRiotFilterBody() - } - FilterService.FilterPreset.NoFilter -> { - FilterFactory.createDefaultFilterBody() - } - } - - val roomFilter = when (filterPreset) { - FilterService.FilterPreset.RiotFilter -> { - FilterFactory.createRiotRoomFilter() - } - FilterService.FilterPreset.NoFilter -> { - FilterFactory.createDefaultRoomFilter() - } - } - - val updated = filterRepository.storeFilter(filterBody, roomFilter) - - if (updated) { - saveFilterTask - .configureWith(SaveFilterTask.Params(filterBody)) - .executeBy(taskExecutor) - } + saveFilterTask + .configureWith(SaveFilterTask.Params(filterPreset)) + .executeBy(taskExecutor) } } 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 b98049675e..08985bf17d 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,18 +16,19 @@ package im.vector.matrix.android.internal.session.filter +import im.vector.matrix.android.api.session.sync.FilterService 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 /** - * Save a filter to the server + * Save a filter, in db and if any changes, upload to the server */ internal interface SaveFilterTask : Task { data class Params( - val filter: FilterBody + val filterPreset: FilterService.FilterPreset ) } @@ -37,10 +38,29 @@ internal class DefaultSaveFilterTask @Inject constructor(@UserId private val use ) : SaveFilterTask { override suspend fun execute(params: SaveFilterTask.Params) { - val filterResponse = executeRequest { - // TODO auto retry - apiCall = filterAPI.uploadFilter(userId, params.filter) + val filterBody = when (params.filterPreset) { + FilterService.FilterPreset.RiotFilter -> { + FilterFactory.createRiotFilterBody() + } + FilterService.FilterPreset.NoFilter -> { + FilterFactory.createDefaultFilterBody() + } + } + val roomFilter = when (params.filterPreset) { + FilterService.FilterPreset.RiotFilter -> { + FilterFactory.createRiotRoomFilter() + } + FilterService.FilterPreset.NoFilter -> { + FilterFactory.createDefaultRoomFilter() + } + } + val updated = filterRepository.storeFilter(filterBody, roomFilter) + if (updated) { + val filterResponse = executeRequest { + // TODO auto retry + apiCall = filterAPI.uploadFilter(userId, filterBody) + } + filterRepository.storeFilterId(filterBody, filterResponse.filterId) } - filterRepository.storeFilterId(params.filter, filterResponse.filterId) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt index 092d9ff766..d205ea8a87 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt @@ -21,20 +21,20 @@ internal interface FilterRepository { /** * Return true if the filterBody has changed, or need to be sent to the server */ - fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean + suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean /** * Set the filterId of this filter */ - fun storeFilterId(filterBody: FilterBody, filterId: String) + suspend fun storeFilterId(filterBody: FilterBody, filterId: String) /** * Return filter json or filter id */ - fun getFilter(): String + suspend fun getFilter(): String /** * Return the room filter */ - fun getRoomFilter(): String + suspend fun getRoomFilter(): String } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt index 98ab0b5389..45571286b9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.database.query.getOrCreate 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 java.util.* +import java.util.Date import javax.inject.Inject internal interface GetHomeServerCapabilitiesTask : Task 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 82b928fc54..9121202649 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 @@ -28,7 +28,9 @@ 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.query.where import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.pushers.AddPushRuleTask import im.vector.matrix.android.internal.session.pushers.GetPushRulesTask +import im.vector.matrix.android.internal.session.pushers.RemovePushRuleTask import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleEnableStatusTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -38,6 +40,8 @@ import javax.inject.Inject @SessionScope internal class DefaultPushRuleService @Inject constructor(private val getPushRulesTask: GetPushRulesTask, private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask, + private val addPushRuleTask: AddPushRuleTask, + private val removePushRuleTask: RemovePushRuleTask, private val taskExecutor: TaskExecutor, private val monarchy: Monarchy ) : PushRuleService { @@ -98,6 +102,22 @@ internal class DefaultPushRuleService @Inject constructor(private val getPushRul .executeBy(taskExecutor) } + override fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable { + return addPushRuleTask + .configureWith(AddPushRuleTask.Params(kind, pushRule)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback): Cancelable { + return removePushRuleTask + .configureWith(RemovePushRuleTask.Params(kind, pushRule)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) { synchronized(listeners) { listeners.remove(listener) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddPushRuleTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddPushRuleTask.kt new file mode 100644 index 0000000000..99992ef4dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/AddPushRuleTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.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 +import javax.inject.Inject + +internal interface AddPushRuleTask : Task { + data class Params( + val kind: RuleKind, + val pushRule: PushRule + ) +} + +internal class DefaultAddPushRuleTask @Inject constructor(private val pushRulesApi: PushRulesApi) + : AddPushRuleTask { + + override suspend fun execute(params: AddPushRuleTask.Params) { + return executeRequest { + apiCall = pushRulesApi.addRule(params.kind.value, params.pushRule.ruleId, params.pushRule) + } + } +} 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 243e4d4b03..8c7e9fb263 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 @@ -32,7 +32,7 @@ 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.* +import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject 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 7aa06c0275..1564363e1b 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 @@ -25,6 +25,8 @@ import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.internal.session.notification.DefaultProcessEventForPushTask import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask +import im.vector.matrix.android.internal.session.room.notification.DefaultSetRoomNotificationStateTask +import im.vector.matrix.android.internal.session.room.notification.SetRoomNotificationStateTask import retrofit2.Retrofit @Module @@ -67,6 +69,15 @@ internal abstract class PushersModule { @Binds abstract fun bindUpdatePushRuleEnableStatusTask(updatePushRuleEnableStatusTask: DefaultUpdatePushRuleEnableStatusTask): UpdatePushRuleEnableStatusTask + @Binds + abstract fun bindAddPushRuleTask(addPushRuleTask: DefaultAddPushRuleTask): AddPushRuleTask + + @Binds + abstract fun bindRemovePushRuleTask(removePushRuleTask: DefaultRemovePushRuleTask): RemovePushRuleTask + + @Binds + abstract fun bindSetRoomNotificationStateTask(setRoomNotificationStateTask: DefaultSetRoomNotificationStateTask): SetRoomNotificationStateTask + @Binds abstract fun bindPushRuleService(pushRuleService: DefaultPushRuleService): PushRuleService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePushRuleTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePushRuleTask.kt new file mode 100644 index 0000000000..c4938fa0cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/RemovePushRuleTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.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 +import javax.inject.Inject + +internal interface RemovePushRuleTask : Task { + data class Params( + val kind: RuleKind, + val pushRule: PushRule + ) +} + +internal class DefaultRemovePushRuleTask @Inject constructor(private val pushRulesApi: PushRulesApi) + : RemovePushRuleTask { + + override suspend fun execute(params: RemovePushRuleTask.Params) { + return executeRequest { + apiCall = pushRulesApi.deleteRule(params.kind.value, params.pushRule.ruleId) + } + } +} 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 fea827fd25..cca20fc5fc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.relation.RelationService +import im.vector.matrix.android.api.session.room.notification.RoomPushRuleService import im.vector.matrix.android.api.session.room.reporting.ReportingService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.DraftService @@ -49,7 +50,8 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val readService: ReadService, private val cryptoService: CryptoService, private val relationService: RelationService, - private val roomMembersService: MembershipService) : + private val roomMembersService: MembershipService, + private val roomPushRuleService: RoomPushRuleService) : Room, TimelineService by timelineService, SendService by sendService, @@ -58,7 +60,8 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, ReportingService by reportingService, ReadService by readService, RelationService by relationService, - MembershipService by roomMembersService { + MembershipService by roomMembersService, + RoomPushRuleService by roomPushRuleService { override fun getRoomSummaryLive(): LiveData> { val liveData = monarchy.findAllMappedWithChanges( 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 0b18279aa8..c9d5aeb6bb 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 @@ -21,7 +21,7 @@ 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 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.mapper.ContentMapper 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 @@ -41,8 +41,8 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona fun resolve(roomId: String): String? { var res: String? = null monarchy.doWithRealm { realm -> - val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()?.asDomain() - res = roomName?.content.toModel()?.avatarUrl + val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev() + res = ContentMapper.map(roomName?.content).toModel()?.avatarUrl if (!res.isNullOrEmpty()) { return@doWithRealm } @@ -60,6 +60,6 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona } private fun EventEntity?.toRoomMember(): RoomMember? { - return this?.asDomain()?.content?.toModel() + return ContentMapper.map(this?.content).toModel() } } 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 e2199782f4..30a2948f68 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 @@ -22,6 +22,7 @@ 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.notification.DefaultRoomPushRuleService import im.vector.matrix.android.internal.session.room.read.DefaultReadService import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService import im.vector.matrix.android.internal.session.room.reporting.DefaultReportingService @@ -44,7 +45,8 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona private val reportingServiceFactory: DefaultReportingService.Factory, private val readServiceFactory: DefaultReadService.Factory, private val relationServiceFactory: DefaultRelationService.Factory, - private val membershipServiceFactory: DefaultMembershipService.Factory) : + private val membershipServiceFactory: DefaultMembershipService.Factory, + private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory) : RoomFactory { override fun create(roomId: String): Room { @@ -60,7 +62,8 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona readServiceFactory.create(roomId), cryptoService, relationServiceFactory.create(roomId), - membershipServiceFactory.create(roomId) + membershipServiceFactory.create(roomId), + roomPushRuleServiceFactory.create(roomId) ) } } 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 0d28720ec6..1158c08984 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 @@ -21,7 +21,7 @@ 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 import im.vector.matrix.android.api.session.room.model.RoomTopicContent -import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.mapper.ContentMapper 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 @@ -65,8 +65,10 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId roomId: String, membership: Membership? = null, roomSummary: RoomSyncSummary? = null, - unreadNotifications: RoomSyncUnreadNotifications? = null) { - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + unreadNotifications: RoomSyncUnreadNotifications? = null, + updateMembers: Boolean = false) { + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) if (roomSummary != null) { if (roomSummary.heroes.isNotEmpty()) { @@ -88,24 +90,27 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId } val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) - val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() + val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() 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, userId) - .findAll() - .asSequence() - .map { it.stateKey } + // avoid this call if we are sure there are unread events + || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) - roomSummaryEntity.topic = lastTopicEvent?.content.toModel()?.topic + roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel()?.topic roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent - roomSummaryEntity.otherMemberIds.clear() - roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) + + if (updateMembers) { + val otherRoomMembers = RoomMembers(realm, roomId) + .queryRoomMembersEvent() + .notEqualTo(EventEntityFields.STATE_KEY, userId) + .findAll() + .asSequence() + .map { it.stateKey } + + roomSummaryEntity.otherMemberIds.clear() + roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt index d952915d2c..7d9332ee84 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt @@ -74,7 +74,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP it.updateSenderData() } roomEntity.areAllMembersLoaded = true - roomSummaryUpdater.update(realm, roomId) + roomSummaryUpdater.update(realm, roomId, updateMembers = true) } } 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 965bd21cf4..2271631932 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 @@ -22,7 +22,7 @@ import im.vector.matrix.android.R 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.* -import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.mapper.ContentMapper 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.RoomEntity @@ -56,20 +56,20 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: var name: CharSequence? = null monarchy.doWithRealm { realm -> val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).prev()?.asDomain() - name = roomName?.content.toModel()?.name + val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).prev() + name = ContentMapper.map(roomName?.content).toModel()?.name if (!name.isNullOrEmpty()) { return@doWithRealm } - val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()?.asDomain() - name = canonicalAlias?.content.toModel()?.canonicalAlias + val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev() + name = ContentMapper.map(canonicalAlias?.content).toModel()?.canonicalAlias if (!name.isNullOrEmpty()) { return@doWithRealm } - val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()?.asDomain() - name = aliases?.content.toModel()?.aliases?.firstOrNull() + val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev() + name = ContentMapper.map(aliases?.content).toModel()?.aliases?.firstOrNull() if (!name.isNullOrEmpty()) { return@doWithRealm } @@ -132,6 +132,6 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: } private fun EventEntity?.toRoomMember(): RoomMember? { - return this?.asDomain()?.content?.toModel() + return ContentMapper.map(this?.content).toModel() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index b50424b343..9fba1d8f02 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -73,6 +73,7 @@ internal class RoomMembers(private val realm: Realm, return EventEntity .where(realm, roomId, EventType.STATE_ROOM_MEMBER) .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) + .isNotNull(EventEntityFields.STATE_KEY) .distinct(EventEntityFields.STATE_KEY) .isNotNull(EventEntityFields.CONTENT) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/DefaultRoomPushRuleService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/DefaultRoomPushRuleService.kt new file mode 100644 index 0000000000..7cb7452244 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/DefaultRoomPushRuleService.kt @@ -0,0 +1,72 @@ +/* + * 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.notification + +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.api.MatrixCallback +import im.vector.matrix.android.api.pushrules.RuleScope +import im.vector.matrix.android.api.session.room.notification.RoomNotificationState +import im.vector.matrix.android.api.session.room.notification.RoomPushRuleService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.database.model.PushRuleEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith + +internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted private val roomId: String, + private val setRoomNotificationStateTask: SetRoomNotificationStateTask, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor) + : RoomPushRuleService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): RoomPushRuleService + } + + override fun getLiveRoomNotificationState(): LiveData { + return Transformations.map(getPushRuleForRoom()) { + it?.toRoomNotificationState() ?: RoomNotificationState.ALL_MESSAGES + } + } + + override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback): Cancelable { + return setRoomNotificationStateTask + .configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + private fun getPushRuleForRoom(): LiveData { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> + PushRuleEntity.where(realm, scope = RuleScope.GLOBAL, ruleId = roomId) + }, + { result -> + result.toRoomPushRule() + } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/RoomPushRule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/RoomPushRule.kt new file mode 100644 index 0000000000..b07f94f0f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/RoomPushRule.kt @@ -0,0 +1,25 @@ +/* + * 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.notification + +import im.vector.matrix.android.api.pushrules.RuleKind +import im.vector.matrix.android.api.pushrules.rest.PushRule + +internal data class RoomPushRule( + val kind: RuleKind, + val rule: PushRule +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/RoomPushRuleMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/RoomPushRuleMapper.kt new file mode 100644 index 0000000000..d08d346a1f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/RoomPushRuleMapper.kt @@ -0,0 +1,101 @@ +/* + * 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.notification + +import im.vector.matrix.android.api.pushrules.* +import im.vector.matrix.android.api.pushrules.rest.PushCondition +import im.vector.matrix.android.api.pushrules.rest.PushRule +import im.vector.matrix.android.api.session.room.notification.RoomNotificationState +import im.vector.matrix.android.internal.database.mapper.PushRulesMapper +import im.vector.matrix.android.internal.database.model.PushRuleEntity + +internal fun PushRuleEntity.toRoomPushRule(): RoomPushRule? { + val kind = parent?.firstOrNull()?.kind + val pushRule = when (kind) { + RuleSetKey.OVERRIDE -> { + PushRulesMapper.map(this) + } + RuleSetKey.ROOM -> { + PushRulesMapper.mapRoomRule(this) + } + else -> null + } + return if (pushRule == null || kind == null) { + null + } else { + RoomPushRule(kind, pushRule) + } +} + +internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule? { + return when { + this == RoomNotificationState.ALL_MESSAGES -> null + this == RoomNotificationState.ALL_MESSAGES_NOISY -> { + val rule = PushRule( + actions = listOf(Action.Notify, Action.Sound()).toJson(), + enabled = true, + ruleId = roomId + ) + return RoomPushRule(RuleSetKey.ROOM, rule) + } + else -> { + val condition = PushCondition( + kind = Condition.Kind.event_match.value, + key = "room_id", + pattern = roomId + ) + val rule = PushRule( + actions = listOf(Action.DoNotNotify).toJson(), + enabled = true, + ruleId = roomId, + conditions = listOf(condition) + ) + val kind = if (this == RoomNotificationState.MUTE) { + RuleSetKey.OVERRIDE + } else { + RuleSetKey.ROOM + } + return RoomPushRule(kind, rule) + } + } +} + +internal fun RoomPushRule.toRoomNotificationState(): RoomNotificationState { + return if (rule.enabled) { + val actions = rule.getActions() + if (actions.contains(Action.DoNotNotify)) { + if (kind == RuleSetKey.OVERRIDE) { + RoomNotificationState.MUTE + } else { + RoomNotificationState.MENTIONS_ONLY + } + } else if (actions.contains(Action.Notify)) { + val hasSoundAction = actions.find { + it is Action.Sound + } != null + if (hasSoundAction) { + RoomNotificationState.ALL_MESSAGES_NOISY + } else { + RoomNotificationState.ALL_MESSAGES + } + } else { + RoomNotificationState.ALL_MESSAGES + } + } else { + RoomNotificationState.ALL_MESSAGES + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/SetRoomNotificationStateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/SetRoomNotificationStateTask.kt new file mode 100644 index 0000000000..0362a6607f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/notification/SetRoomNotificationStateTask.kt @@ -0,0 +1,54 @@ +/* + * 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.notification + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.pushrules.RuleScope +import im.vector.matrix.android.api.session.room.notification.RoomNotificationState +import im.vector.matrix.android.internal.database.model.PushRuleEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.pushers.AddPushRuleTask +import im.vector.matrix.android.internal.session.pushers.RemovePushRuleTask +import im.vector.matrix.android.internal.task.Task +import io.realm.Realm +import javax.inject.Inject + +internal interface SetRoomNotificationStateTask : Task { + data class Params( + val roomId: String, + val roomNotificationState: RoomNotificationState + ) +} + +internal class DefaultSetRoomNotificationStateTask @Inject constructor(private val monarchy: Monarchy, + private val removePushRuleTask: RemovePushRuleTask, + private val addPushRuleTask: AddPushRuleTask) + : SetRoomNotificationStateTask { + + override suspend fun execute(params: SetRoomNotificationStateTask.Params) { + val currentRoomPushRule = Realm.getInstance(monarchy.realmConfiguration).use { + PushRuleEntity.where(it, scope = RuleScope.GLOBAL, ruleId = params.roomId).findFirst()?.toRoomPushRule() + } + if (currentRoomPushRule != null) { + removePushRuleTask.execute(RemovePushRuleTask.Params(currentRoomPushRule.kind, currentRoomPushRule.rule)) + } + val newRoomPushRule = params.roomNotificationState.toRoomPushRule(params.roomId) + if (newRoomPushRule != null) { + addPushRuleTask.execute(AddPushRuleTask.Params(newRoomPushRule.kind, newRoomPushRule.rule)) + } + } +} 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 49c813ece6..3fa0dcdca1 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 @@ -39,7 +39,6 @@ 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.* import javax.inject.Inject /** @@ -119,7 +118,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use permalink, stringProvider.getString(R.string.message_reply_to_prefix), userLink, - originalEvent.senderName ?: originalEvent.root.senderId, + originalEvent.getDisambiguatedDisplayName(), body.takeFormatted(), createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 606c20e8cb..4127e43540 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -52,7 +52,8 @@ import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort import timber.log.Timber -import java.util.* +import java.util.Collections +import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt index 260f98d97f..592191975e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt @@ -31,7 +31,7 @@ import java.security.KeyPairGenerator import java.security.KeyStore import java.security.KeyStoreException import java.security.SecureRandom -import java.util.* +import java.util.Calendar import javax.crypto.* import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.IvParameterSpec 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 5fb25834c0..4a003eb7d9 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 @@ -67,6 +67,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch } suspend fun handle(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService? = null) { + Timber.v("Execute transaction from $this") monarchy.awaitTransaction { realm -> handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter) handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter) @@ -133,6 +134,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomEntity.membership = Membership.JOIN // State event + if (roomSync.state != null && roomSync.state.events.isNotEmpty()) { val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt() ?: Int.MIN_VALUE @@ -146,7 +148,6 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch } } } - if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) { val chunkEntity = handleTimelineEvents( realm, @@ -157,14 +158,19 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch ) roomEntity.addOrUpdate(chunkEntity) } - roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications) + val hasRoomMember = roomSync.state?.events?.firstOrNull { + it.type == EventType.STATE_ROOM_MEMBER + } != null || roomSync.timeline?.events?.firstOrNull { + it.type == EventType.STATE_ROOM_MEMBER + } != null + + roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications, updateMembers = hasRoomMember) return roomEntity } private fun handleInvitedRoom(realm: Realm, roomId: String, - roomSync: - InvitedRoomSync): RoomEntity { + roomSync: InvitedRoomSync): RoomEntity { Timber.v("Handle invited sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) roomEntity.membership = Membership.INVITE @@ -172,7 +178,10 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) roomEntity.addOrUpdate(chunkEntity) } - roomSummaryUpdater.update(realm, roomId, Membership.INVITE) + val hasRoomMember = roomSync.inviteState?.events?.firstOrNull { + it.type == EventType.STATE_ROOM_MEMBER + } != null + roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = hasRoomMember) return roomEntity } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt index f56ee3352f..350f2a1d83 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTokenStore.kt @@ -16,27 +16,24 @@ package im.vector.matrix.android.internal.session.sync +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.model.SyncEntity -import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm -import io.realm.RealmConfiguration import javax.inject.Inject -internal class SyncTokenStore @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { +internal class SyncTokenStore @Inject constructor(private val monarchy: Monarchy) { fun getLastToken(): String? { - val realm = Realm.getInstance(realmConfiguration) - val token = realm.where(SyncEntity::class.java).findFirst()?.nextBatch - realm.close() - return token + return Realm.getInstance(monarchy.realmConfiguration).use { + it.where(SyncEntity::class.java).findFirst()?.nextBatch + } } - fun saveToken(token: String?) { - val realm = Realm.getInstance(realmConfiguration) - realm.executeTransaction { + suspend fun saveToken(token: String?) { + monarchy.awaitTransaction { val sync = SyncEntity(token) it.insertOrUpdate(sync) } - realm.close() } } 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 56b96b428d..56bc005805 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 @@ -27,10 +27,9 @@ 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.sync.model.accountdata.* import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper +import im.vector.matrix.android.internal.session.user.accountdata.SaveIgnoredUsersTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -44,6 +43,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc private val directChatsHelper: DirectChatsHelper, private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val savePushRulesTask: SavePushRulesTask, + private val saveIgnoredUsersTask: SaveIgnoredUsersTask, private val taskExecutor: TaskExecutor) { suspend fun handle(accountData: UserAccountDataSync?, invites: Map?) { @@ -51,9 +51,18 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc when (it) { is UserAccountDataDirectMessages -> handleDirectChatRooms(it) is UserAccountDataPushRules -> handlePushRules(it) - else -> return@forEach + is UserAccountDataIgnoredUsers -> handleIgnoredUsers(it) + is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}") + else -> error("Missing code here!") } } + + // TODO Store all account data, app can be interested of it + // accountData?.list?.forEach { + // it.toString() + // MoshiProvider.providesMoshi() + // } + monarchy.doWithRealm { realm -> synchronizeWithServerIfNeeded(realm, invites) } @@ -114,4 +123,11 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor) } } + + private fun handleIgnoredUsers(userAccountDataIgnoredUsers: UserAccountDataIgnoredUsers) { + saveIgnoredUsersTask + .configureWith(SaveIgnoredUsersTask.Params(userAccountDataIgnoredUsers.content.ignoredUsers.keys.toList())) + .executeBy(taskExecutor) + // TODO If not initial sync, we should execute a init sync + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/SyncResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/SyncResponse.kt index d084dcdadd..9e5cc2cfa4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/SyncResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/SyncResponse.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.sync.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataSync // SyncResponse represents the request response for server sync v2. @JsonClass(generateAdapter = true) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/IgnoredUsersContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/IgnoredUsersContent.kt new file mode 100644 index 0000000000..ea591d79b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/IgnoredUsersContent.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.api.util.emptyJsonDict + +@JsonClass(generateAdapter = true) +internal data class IgnoredUsersContent( + /** + * Required. The map of users to ignore. UserId -> empty object for future enhancement + */ + @Json(name = "ignored_users") val ignoredUsers: Map +) { + + companion object { + fun createWithUserIds(userIds: List): IgnoredUsersContent { + return IgnoredUsersContent( + ignoredUsers = userIds.associateWith { emptyJsonDict } + ) + } + } +} 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/accountdata/UserAccountData.kt similarity index 81% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountData.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt index 2173d2f4df..55dbad6099 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/accountdata/UserAccountData.kt @@ -14,9 +14,13 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.sync.model +package im.vector.matrix.android.internal.session.sync.model.accountdata -internal interface UserAccountData { +import com.squareup.moshi.Json + +internal abstract class UserAccountData { + + @Json(name = "type") abstract val type: String companion object { const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataDirectMessages.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataDirectMessages.kt similarity index 82% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataDirectMessages.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataDirectMessages.kt index 825a16cb1e..e5c6135bd1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataDirectMessages.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataDirectMessages.kt @@ -14,12 +14,13 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.sync.model +package im.vector.matrix.android.internal.session.sync.model.accountdata import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class UserAccountDataDirectMessages( + @Json(name = "type") override val type: String = TYPE_DIRECT_MESSAGES, @Json(name = "content") val content: Map> -) : UserAccountData +) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataFallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataFallback.kt similarity index 84% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataFallback.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataFallback.kt index 70d28c084f..a8b8235d37 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataFallback.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataFallback.kt @@ -14,12 +14,13 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.sync.model +package im.vector.matrix.android.internal.session.sync.model.accountdata import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class UserAccountDataFallback( + @Json(name = "type") override val type: String, @Json(name = "content") val content: Map -) : UserAccountData +) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIgnoredUsers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIgnoredUsers.kt new file mode 100644 index 0000000000..63a7604305 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIgnoredUsers.kt @@ -0,0 +1,26 @@ +/* + * 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.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataIgnoredUsers( + @Json(name = "type") override val type: String = TYPE_IGNORED_USER_LIST, + @Json(name = "content") val content: IgnoredUsersContent +) : UserAccountData() 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/accountdata/UserAccountDataPushRules.kt similarity index 83% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataPushRules.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataPushRules.kt index 7f357c876b..0d549d1667 100644 --- 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/accountdata/UserAccountDataPushRules.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.sync.model +package im.vector.matrix.android.internal.session.sync.model.accountdata import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -22,5 +22,6 @@ import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse @JsonClass(generateAdapter = true) internal data class UserAccountDataPushRules( + @Json(name = "type") override val type: String = TYPE_PUSH_RULES, @Json(name = "content") val content: GetPushRulesResponse -) : UserAccountData +) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataSync.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataSync.kt similarity index 91% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataSync.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataSync.kt index 4b9e9d652d..c7f8bfa4c2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/UserAccountDataSync.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataSync.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.sync.model +package im.vector.matrix.android.internal.session.sync.model.accountdata import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index be330bfc36..d314c8d108 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -29,9 +29,12 @@ import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.IgnoredUserEntity +import im.vector.matrix.android.internal.database.model.IgnoredUserEntityFields import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.model.UserEntityFields import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.user.accountdata.UpdateIgnoredUserIdsTask import im.vector.matrix.android.internal.session.user.model.SearchUserTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -40,8 +43,8 @@ import javax.inject.Inject internal class DefaultUserService @Inject constructor(private val monarchy: Monarchy, private val searchUserTask: SearchUserTask, + private val updateIgnoredUserIdsTask: UpdateIgnoredUserIdsTask, private val taskExecutor: TaskExecutor) : UserService { - private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory by lazy { monarchy.createDataSourceFactory { realm -> realm.where(UserEntity::class.java) @@ -62,7 +65,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona override fun getUser(userId: String): User? { val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } - ?: return null + ?: return null return userEntity.asDomain() } @@ -117,4 +120,33 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona } .executeBy(taskExecutor) } + + override fun liveIgnoredUsers(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm -> + realm.where(IgnoredUserEntity::class.java) + .isNotEmpty(IgnoredUserEntityFields.USER_ID) + .sort(IgnoredUserEntityFields.USER_ID) + }, + { getUser(it.userId) ?: User(userId = it.userId) } + ) + } + + override fun ignoreUserIds(userIds: List, callback: MatrixCallback): Cancelable { + val params = UpdateIgnoredUserIdsTask.Params(userIdsToIgnore = userIds.toList()) + return updateIgnoredUserIdsTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun unIgnoreUserIds(userIds: List, callback: MatrixCallback): Cancelable { + val params = UpdateIgnoredUserIdsTask.Params(userIdsToUnIgnore = userIds.toList()) + return updateIgnoredUserIdsTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt index a997c616f3..51c296ba6e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt @@ -21,6 +21,10 @@ import dagger.Module import dagger.Provides import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.user.accountdata.DefaultSaveIgnoredUsersTask +import im.vector.matrix.android.internal.session.user.accountdata.DefaultUpdateIgnoredUserIdsTask +import im.vector.matrix.android.internal.session.user.accountdata.SaveIgnoredUsersTask +import im.vector.matrix.android.internal.session.user.accountdata.UpdateIgnoredUserIdsTask import im.vector.matrix.android.internal.session.user.model.DefaultSearchUserTask import im.vector.matrix.android.internal.session.user.model.SearchUserTask import retrofit2.Retrofit @@ -43,4 +47,10 @@ internal abstract class UserModule { @Binds abstract fun bindSearchUserTask(searchUserTask: DefaultSearchUserTask): SearchUserTask + + @Binds + abstract fun bindSaveIgnoredUsersTask(task: DefaultSaveIgnoredUsersTask): SaveIgnoredUsersTask + + @Binds + abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/SaveIgnoredUsersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/SaveIgnoredUsersTask.kt new file mode 100644 index 0000000000..c9a3eef440 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/SaveIgnoredUsersTask.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.IgnoredUserEntity +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction +import javax.inject.Inject + +/** + * Save the ignored users list in DB + */ +internal interface SaveIgnoredUsersTask : Task { + data class Params( + val userIds: List + ) +} + +internal class DefaultSaveIgnoredUsersTask @Inject constructor(private val monarchy: Monarchy) : SaveIgnoredUsersTask { + + override suspend fun execute(params: SaveIgnoredUsersTask.Params) { + monarchy.awaitTransaction { realm -> + // clear current ignored users + realm.where(IgnoredUserEntity::class.java) + .findAll() + .deleteAllFromRealm() + + // And save the new received list + params.userIds.forEach { realm.createObject(IgnoredUserEntity::class.java).apply { userId = it } } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt new file mode 100644 index 0000000000..075eeb23d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateIgnoredUserIdsTask.kt @@ -0,0 +1,68 @@ +/* + * 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.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.IgnoredUserEntity +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.accountdata.IgnoredUsersContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface UpdateIgnoredUserIdsTask : Task { + + data class Params( + val userIdsToIgnore: List = emptyList(), + val userIdsToUnIgnore: List = emptyList() + ) +} + +internal class DefaultUpdateIgnoredUserIdsTask @Inject constructor(private val accountDataApi: AccountDataAPI, + private val monarchy: Monarchy, + private val saveIgnoredUsersTask: SaveIgnoredUsersTask, + @UserId private val userId: String) : UpdateIgnoredUserIdsTask { + + override suspend fun execute(params: UpdateIgnoredUserIdsTask.Params) { + // Get current list + val ignoredUserIds = monarchy.fetchAllMappedSync( + { realm -> realm.where(IgnoredUserEntity::class.java) }, + { it.userId } + ).toMutableSet() + + val original = ignoredUserIds.toList() + + ignoredUserIds.removeAll { it in params.userIdsToUnIgnore } + ignoredUserIds.addAll(params.userIdsToIgnore) + + if (original == ignoredUserIds) { + // No change + return + } + + val list = ignoredUserIds.toList() + val body = IgnoredUsersContent.createWithUserIds(list) + + executeRequest { + apiCall = accountDataApi.setAccountData(userId, UserAccountData.TYPE_IGNORED_USER_LIST, body) + } + + // Update the DB right now (do not wait for the sync to come back with updated data, for a faster UI update) + saveIgnoredUsersTask.execute(SaveIgnoredUsersTask.Params(list)) + } +} 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 5c0dac1125..9fa71005ff 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 @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.session.user.accountdata 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.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -29,6 +29,7 @@ internal interface UpdateUserAccountDataTask : Task> ) : Params { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt index 058a862bc8..2df2bd2bf2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt @@ -36,7 +36,7 @@ import java.security.* import java.security.cert.CertificateException import java.security.spec.AlgorithmParameterSpec import java.security.spec.RSAKeyGenParameterSpec -import java.util.* +import java.util.Calendar import java.util.zip.GZIPOutputStream import javax.crypto.* import javax.crypto.spec.GCMParameterSpec diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt index 4a46a43f03..31da372bbe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.util import im.vector.matrix.android.api.MatrixPatterns import timber.log.Timber -import java.util.* +import java.util.Locale /** * Convert a string to an UTF8 String diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml index 85643c7ade..2566ee780c 100644 --- a/matrix-sdk-android/src/main/res/values-bg/strings.xml +++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml @@ -172,4 +172,5 @@ Изпращане на съобщение… Изчисти опашката за изпращане + %1$s оттегли поканата за присъединяване на %2$s към стаята diff --git a/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml b/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000..f457e30ed0 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,5 @@ + + + Spanner + Aeroplane + diff --git a/matrix-sdk-android/src/main/res/values-en-rUS/strings.xml b/matrix-sdk-android/src/main/res/values-en-rUS/strings.xml deleted file mode 100644 index 09e9dfc111..0000000000 --- a/matrix-sdk-android/src/main/res/values-en-rUS/strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Wrench - Airplane - - - - diff --git a/matrix-sdk-android/src/main/res/values-ko/strings.xml b/matrix-sdk-android/src/main/res/values-ko/strings.xml index 959ff8a96e..9dfbb6609b 100644 --- a/matrix-sdk-android/src/main/res/values-ko/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ko/strings.xml @@ -12,8 +12,8 @@ %1$s님이 떠났습니다 %1$s님이 초대를 거부했습니다 %1$s님이 %2$s님을 추방했습니다 - %1$s님이 %2$s님의 차단을 풀었습니다 - %1$s님이 %2$s님을 차단했습니다 + %1$s님이 %2$s님의 출입 금지를 풀었습니다 + %1$s님이 %2$s님을 출입 금지했습니다 %1$s님이 %2$s님의 초대를 취소했습니다 %1$s님이 아바타를 변경했습니다 %1$s님이 표시 이름을 %2$s(으)로 설정했습니다 diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 604bb13a41..ce26c22137 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -170,7 +170,7 @@ Glasses - Spanner + Wrench Santa @@ -208,7 +208,7 @@ Bicycle - Aeroplane + Airplane Rocket diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt index f98af53333..17543e9d25 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushRuleActionsTest.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.api.pushrules import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.internal.di.MoshiProvider -import org.junit.Assert +import org.junit.Assert.* import org.junit.Test class PushRuleActionsTest { @@ -63,22 +63,17 @@ class PushRuleActionsTest { val pushRule = MoshiProvider.providesMoshi().adapter(PushRule::class.java).fromJson(rawPushRule) - Assert.assertNotNull("Should have parsed the rule", pushRule) - Assert.assertNotNull("Failed to parse actions", Action.mapFrom(pushRule!!)) + assertNotNull("Should have parsed the rule", pushRule) - val actions = Action.mapFrom(pushRule) - Assert.assertEquals(3, actions!!.size) + val actions = pushRule!!.getActions() + assertEquals(3, actions.size) - Assert.assertEquals("First action should be notify", Action.Type.NOTIFY, actions[0].type) + assertTrue("First action should be notify", actions[0] is Action.Notify) - Assert.assertEquals("Second action should be tweak", Action.Type.SET_TWEAK, actions[1].type) - Assert.assertEquals("Second action tweak key should be sound", "sound", actions[1].tweak_action) - Assert.assertEquals("Second action should have default as stringValue", "default", actions[1].stringValue) - Assert.assertNull("Second action boolValue should be null", actions[1].boolValue) + assertTrue("Second action should be sound", actions[1] is Action.Sound) + assertEquals("Second action should have default sound", "default", (actions[1] as Action.Sound).sound) - Assert.assertEquals("Third action should be tweak", Action.Type.SET_TWEAK, actions[2].type) - Assert.assertEquals("Third action tweak key should be highlight", "highlight", actions[2].tweak_action) - Assert.assertEquals("Third action tweak param should be false", false, actions[2].boolValue) - Assert.assertNull("Third action stringValue should be null", actions[2].stringValue) + assertTrue("Third action should be highlight", actions[2] is Action.Highlight) + assertEquals("Third action tweak param should be false", false, (actions[2] as Action.Highlight).highlight) } } diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt index 42e7e850b3..a29f5d5542 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt @@ -16,23 +16,15 @@ package im.vector.matrix.android.api.pushrules -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.RoomService -import im.vector.matrix.android.api.session.room.model.* -import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams +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.model.message.MessageTextContent -import im.vector.matrix.android.api.session.room.send.UserDraft -import im.vector.matrix.android.api.session.room.timeline.Timeline -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.api.session.room.timeline.TimelineSettings -import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.api.util.Optional +import io.mockk.every +import io.mockk.mockk import org.junit.Assert import org.junit.Test @@ -133,17 +125,20 @@ class PushrulesConditionTest { val conditionEqual3Bis = RoomMemberCountCondition("==3") val conditionLessThan3 = RoomMemberCountCondition("<3") - val session = MockRoomService() + val room2JoinedId = "2joined" + val room3JoinedId = "3joined" - Event( - type = "m.room.message", - eventId = "mx0", - content = MessageTextContent("m.text", "A").toContent(), - originServerTs = 0, - roomId = "2joined").also { - Assert.assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, session)) - Assert.assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, session)) - Assert.assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, session)) + val roomStub2Joined = mockk { + every { getNumberOfJoinedMembers() } returns 2 + } + + val roomStub3Joined = mockk { + every { getNumberOfJoinedMembers() } returns 3 + } + + val sessionStub = mockk { + every { getRoom(room2JoinedId) } returns roomStub2Joined + every { getRoom(room3JoinedId) } returns roomStub3Joined } Event( @@ -151,10 +146,21 @@ class PushrulesConditionTest { eventId = "mx0", content = MessageTextContent("m.text", "A").toContent(), originServerTs = 0, - roomId = "3joined").also { - Assert.assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, session)) - Assert.assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, session)) - Assert.assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, session)) + roomId = room2JoinedId).also { + Assert.assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, sessionStub)) + Assert.assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, sessionStub)) + Assert.assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, sessionStub)) + } + + Event( + type = "m.room.message", + eventId = "mx0", + content = MessageTextContent("m.text", "A").toContent(), + originServerTs = 0, + roomId = room3JoinedId).also { + Assert.assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, sessionStub)) + Assert.assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, sessionStub)) + Assert.assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, sessionStub)) } } @@ -171,208 +177,4 @@ class PushrulesConditionTest { Assert.assertTrue("Notice", conditionEqual.isSatisfied(it)) } } - - class MockRoomService() : RoomService { - override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - 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? { - return when (roomId) { - "2joined" -> MockRoom(roomId, 2) - "3joined" -> MockRoom(roomId, 3) - else -> null - } - } - - override fun liveRoomSummaries(): LiveData> { - return MutableLiveData() - } - - override fun markAllAsRead(roomIds: List, callback: MatrixCallback): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - } - - class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room { - override fun getReadMarkerLive(): LiveData> { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun getMyReadReceiptLive(): LiveData> { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - 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 getTimeLineEventLive(eventId: String): LiveData> { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun getNumberOfJoinedMembers(): Int { - return _numberOfJoinedMembers - } - - override fun getRoomSummaryLive(): LiveData> { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun roomSummary(): RoomSummary? { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun getTimeLineEvent(eventId: String): TimelineEvent? { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun sendMedia(attachment: ContentAttachmentData): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun sendMedias(attachments: List): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun redactEvent(event: Event, reason: String?): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun markAllAsRead(callback: MatrixCallback) { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun setReadReceipt(eventId: String, callback: MatrixCallback) { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun isEventRead(eventId: String): Boolean { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun getRoomMember(userId: String): RoomMember? { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun getRoomMemberIdsLive(): LiveData> { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - 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(viaServers: List, callback: MatrixCallback): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun leave(callback: MatrixCallback): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun updateTopic(topic: String, callback: MatrixCallback) { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun sendReaction(targetEventId: String, reaction: String): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun undoReaction(targetEventId: String, reaction: String): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun editTextMessage(targetEventId: String, msgType: String, newBodyText: String, - newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun getEventSummaryLive(eventId: String): LiveData> { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun isEncrypted(): Boolean { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun encryptionAlgorithm(): String? { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - - override fun shouldEncryptForInvitedMembers(): Boolean { - TODO("not implemented") // To change body of created functions use File | Settings | File Templates. - } - } } diff --git a/tools/import_from_riot.sh b/tools/import_from_riot.sh index 2e4b332a3c..271a872a48 100755 --- a/tools/import_from_riot.sh +++ b/tools/import_from_riot.sh @@ -32,7 +32,7 @@ cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-da/strings.xml ./mat cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-de/strings.xml ./matrix-sdk-android/src/main/res/values-de/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-el/strings.xml ./matrix-sdk-android/src/main/res/values-el/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eo/strings.xml ./matrix-sdk-android/src/main/res/values-eo/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-en-rUS/strings.xml ./matrix-sdk-android/src/main/res/values-en-rUS/strings.xml +cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-en-rGB/strings.xml ./matrix-sdk-android/src/main/res/values-en-rGB/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es/strings.xml ./matrix-sdk-android/src/main/res/values-es/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es-rMX/strings.xml ./matrix-sdk-android/src/main/res/values-es-rMX/strings.xml cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eu/strings.xml ./matrix-sdk-android/src/main/res/values-eu/strings.xml @@ -102,6 +102,7 @@ cp ../riot-android/vector/src/main/res/values-ro/strings.xml ./vector/src cp ../riot-android/vector/src/main/res/values-ru/strings.xml ./vector/src/main/res/values-ru/strings.xml cp ../riot-android/vector/src/main/res/values-sk/strings.xml ./vector/src/main/res/values-sk/strings.xml cp ../riot-android/vector/src/main/res/values-sq/strings.xml ./vector/src/main/res/values-sq/strings.xml +cp ../riot-android/vector/src/main/res/values-sr/strings.xml ./vector/src/main/res/values-sr/strings.xml cp ../riot-android/vector/src/main/res/values-te/strings.xml ./vector/src/main/res/values-te/strings.xml cp ../riot-android/vector/src/main/res/values-th/strings.xml ./vector/src/main/res/values-th/strings.xml cp ../riot-android/vector/src/main/res/values-tlh/strings.xml ./vector/src/main/res/values-tlh/strings.xml diff --git a/vector/build.gradle b/vector/build.gradle index 3ef125d331..e425d53a62 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 7 +ext.versionMinor = 8 ext.versionPatch = 0 static def getGitTimestamp() { @@ -217,9 +217,10 @@ android { dependencies { def epoxy_version = '3.8.0' + def fragment_version = '1.2.0-rc01' def arrow_version = "0.8.2" def coroutines_version = "1.3.2" - def markwon_version = '3.1.0' + def markwon_version = '4.1.2' def big_image_viewer_version = '1.5.6' def glide_version = '4.10.0' def moshi_version = '1.8.0' @@ -234,6 +235,8 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation 'androidx.appcompat:appcompat:1.1.0' + implementation "androidx.fragment:fragment:$fragment_version" + implementation "androidx.fragment:fragment-ktx:$fragment_version" //Do not use beta2 at the moment, as it breaks things implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1' implementation 'androidx.core:core-ktx:1.1.0' @@ -265,7 +268,7 @@ dependencies { implementation("com.airbnb.android:epoxy:$epoxy_version") kapt "com.airbnb.android:epoxy-processor:$epoxy_version" implementation "com.airbnb.android:epoxy-paging:$epoxy_version" - implementation 'com.airbnb.android:mvrx:1.1.0' + implementation 'com.airbnb.android:mvrx:1.3.0' // Work implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01" @@ -283,8 +286,8 @@ dependencies { implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.google.android.material:material:1.1.0-beta01' implementation 'me.gujun.android:span:1.7' - implementation "ru.noties.markwon:core:$markwon_version" - implementation "ru.noties.markwon:html:$markwon_version" + implementation "io.noties.markwon:core:$markwon_version" + implementation "io.noties.markwon:html:$markwon_version" implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.google.android:flexbox:1.1.1' diff --git a/vector/src/androidTest/java/im/vector/riotx/ExampleInstrumentedTest.kt b/vector/src/androidTest/java/im/vector/riotx/ExampleInstrumentedTest.kt deleted file mode 100644 index afed0c783a..0000000000 --- a/vector/src/androidTest/java/im/vector/riotx/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx - -import androidx.test.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getTargetContext() - assertEquals("im.vector.riotx", appContext.packageName) - } -} diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml index 0d071e3465..8bb571902a 100644 --- a/vector/src/debug/AndroidManifest.xml +++ b/vector/src/debug/AndroidManifest.xml @@ -4,6 +4,7 @@ + diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt index bfb9f0654e..6624a05985 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt @@ -26,6 +26,7 @@ import androidx.core.app.Person import butterknife.OnClick import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity class DebugMenuActivity : VectorBaseActivity() { @@ -36,6 +37,11 @@ class DebugMenuActivity : VectorBaseActivity() { startActivity(Intent(this, TestLinkifyActivity::class.java)) } + @OnClick(R.id.debug_show_sas_emoji) + fun showSasEmoji() { + startActivity(Intent(this, DebugSasEmojiActivity::class.java)) + } + @OnClick(R.id.debug_test_notification) fun testNotification() { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/DebugSasEmojiActivity.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/DebugSasEmojiActivity.kt new file mode 100644 index 0000000000..ba1c47c08c --- /dev/null +++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/DebugSasEmojiActivity.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.debug.sas + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import im.vector.matrix.android.api.crypto.getAllVerificationEmojis +import im.vector.riotx.R +import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* + +class DebugSasEmojiActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.fragment_generic_recycler_epoxy) + + val controller = SasEmojiController() + epoxyRecyclerView.setController(controller) + controller.setData(SasState(getAllVerificationEmojis())) + } +} diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt new file mode 100644 index 0000000000..92d9bc0b11 --- /dev/null +++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/ItemSasEmoji.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.debug.sas + +import android.annotation.SuppressLint +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = im.vector.riotx.R.layout.item_sas_emoji) +abstract class ItemSasEmoji : VectorEpoxyModel() { + + @EpoxyAttribute + var index: Int = 0 + @EpoxyAttribute + lateinit var emojiRepresentation: EmojiRepresentation + + @SuppressLint("SetTextI18n") + override fun bind(holder: Holder) { + super.bind(holder) + holder.indexView.text = "$index:" + holder.emojiView.text = emojiRepresentation.emoji + holder.textView.setText(emojiRepresentation.nameResId) + holder.idView.text = holder.idView.resources.getResourceEntryName(emojiRepresentation.nameResId) + } + + class Holder : VectorEpoxyHolder() { + val indexView by bind(im.vector.riotx.R.id.sas_emoji_index) + val emojiView by bind(im.vector.riotx.R.id.sas_emoji) + val textView by bind(im.vector.riotx.R.id.sas_emoji_text) + val idView by bind(im.vector.riotx.R.id.sas_emoji_text_id) + } +} diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.kt new file mode 100644 index 0000000000..daf432fb45 --- /dev/null +++ b/vector/src/debug/java/im/vector/riotx/features/debug/sas/SasEmojiController.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.riotx.features.debug.sas + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation + +data class SasState( + val emojiList: List +) + +class SasEmojiController : TypedEpoxyController() { + + override fun buildModels(data: SasState?) { + if (data == null) return + + data.emojiList.forEachIndexed { idx, emojiRepresentation -> + itemSasEmoji { + id(idx) + index(idx) + emojiRepresentation(emojiRepresentation) + } + } + } +} diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector/src/debug/res/layout/activity_debug_menu.xml index a0acfe9937..01ab061f6a 100644 --- a/vector/src/debug/res/layout/activity_debug_menu.xml +++ b/vector/src/debug/res/layout/activity_debug_menu.xml @@ -47,6 +47,13 @@ android:layout_height="wrap_content" android:text="Test Material theme Dark" /> + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt b/vector/src/main/java/im/vector/riotx/ActiveSessionDataSource.kt similarity index 84% rename from vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt rename to vector/src/main/java/im/vector/riotx/ActiveSessionDataSource.kt index fd6a92e820..4cafdb8d11 100644 --- a/vector/src/main/java/im/vector/riotx/ActiveSessionObservableStore.kt +++ b/vector/src/main/java/im/vector/riotx/ActiveSessionDataSource.kt @@ -19,9 +19,9 @@ package im.vector.riotx import arrow.core.Option import im.vector.matrix.android.api.session.Session -import im.vector.riotx.core.utils.RxStore +import im.vector.riotx.core.utils.BehaviorDataSource import javax.inject.Inject import javax.inject.Singleton @Singleton -class ActiveSessionObservableStore @Inject constructor() : RxStore>() +class ActiveSessionDataSource @Inject constructor() : BehaviorDataSource>() diff --git a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt index 76cbb9ef94..cfbed0ee13 100644 --- a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt @@ -23,9 +23,10 @@ import arrow.core.Option import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.rx.rx -import im.vector.riotx.features.home.HomeRoomListObservableStore +import im.vector.riotx.features.home.HomeRoomListDataSource import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID -import im.vector.riotx.features.home.group.SelectedGroupStore +import im.vector.riotx.features.home.group.SelectedGroupDataSource +import im.vector.riotx.features.home.room.list.ChronologicalRoomComparator import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable @@ -41,9 +42,10 @@ import javax.inject.Singleton */ @Singleton class AppStateHandler @Inject constructor( - private val sessionObservableStore: ActiveSessionObservableStore, - private val homeRoomListObservableStore: HomeRoomListObservableStore, - private val selectedGroupStore: SelectedGroupStore) : LifecycleObserver { + private val sessionDataSource: ActiveSessionDataSource, + private val homeRoomListDataSource: HomeRoomListDataSource, + private val selectedGroupDataSource: SelectedGroupDataSource, + private val chronologicalRoomComparator: ChronologicalRoomComparator) : LifecycleObserver { private val compositeDisposable = CompositeDisposable() @@ -60,39 +62,32 @@ class AppStateHandler @Inject constructor( private fun observeRoomsAndGroup() { Observable .combineLatest, Option, List>( - sessionObservableStore.observe() + sessionDataSource.observe() .observeOn(AndroidSchedulers.mainThread()) .switchMap { it.orNull()?.rx()?.liveRoomSummaries() - ?: Observable.just(emptyList()) + ?: Observable.just(emptyList()) } .throttleLast(300, TimeUnit.MILLISECONDS), - selectedGroupStore.observe(), + selectedGroupDataSource.observe(), BiFunction { rooms, selectedGroupOption -> val selectedGroup = selectedGroupOption.orNull() - val filteredDirectRooms = rooms - .filter { it.isDirect } - .filter { - if (selectedGroup == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) { - true - } else { - it.otherMemberIds - .intersect(selectedGroup.userIds) - .isNotEmpty() - } - } - - val filteredGroupRooms = rooms - .filter { !it.isDirect } - .filter { - selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID - || selectedGroup?.roomIds?.contains(it.roomId) ?: true - } - filteredDirectRooms + filteredGroupRooms + val filteredRooms = rooms.filter { + if (selectedGroup == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) { + true + } else if (it.isDirect) { + it.otherMemberIds + .intersect(selectedGroup.userIds) + .isNotEmpty() + } else { + selectedGroup.roomIds.contains(it.roomId) + } + } + filteredRooms.sortedWith(chronologicalRoomComparator) } ) .subscribe { - homeRoomListObservableStore.post(it) + homeRoomListDataSource.post(it) } .addTo(compositeDisposable) } diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index b1fd6a8485..20a17e55d4 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -55,7 +55,8 @@ import im.vector.riotx.features.version.VersionProvider import im.vector.riotx.push.fcm.FcmHelper import timber.log.Timber import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale import javax.inject.Inject class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider { diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt index da3c041a1c..3eccb668ea 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt @@ -19,7 +19,7 @@ package im.vector.riotx.core.di import arrow.core.Option import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.session.Session -import im.vector.riotx.ActiveSessionObservableStore +import im.vector.riotx.ActiveSessionDataSource import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler import java.util.concurrent.atomic.AtomicReference @@ -28,7 +28,7 @@ import javax.inject.Singleton @Singleton class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator, - private val sessionObservableStore: ActiveSessionObservableStore, + private val sessionObservableStore: ActiveSessionDataSource, private val keyRequestHandler: KeyRequestHandler, private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler ) { diff --git a/vector/src/test/java/im/vector/riotx/ExampleUnitTest.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentKey.kt similarity index 64% rename from vector/src/test/java/im/vector/riotx/ExampleUnitTest.kt rename to vector/src/main/java/im/vector/riotx/core/di/FragmentKey.kt index c51f642a1b..57b5c902e5 100644 --- a/vector/src/test/java/im/vector/riotx/ExampleUnitTest.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentKey.kt @@ -12,22 +12,16 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ - -package im.vector.riotx - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). * - * See [testing documentation](http://d.android.com/tools/testing). */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} + +package im.vector.riotx.core.di + +import androidx.fragment.app.Fragment +import dagger.MapKey +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@Retention(AnnotationRetention.RUNTIME) +@MapKey +annotation class FragmentKey(val value: KClass) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt new file mode 100644 index 0000000000..6ae4619033 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -0,0 +1,197 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.core.di + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment +import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragment +import im.vector.riotx.features.crypto.verification.SASVerificationShortCodeFragment +import im.vector.riotx.features.crypto.verification.SASVerificationStartFragment +import im.vector.riotx.features.crypto.verification.SASVerificationVerifiedFragment +import im.vector.riotx.features.home.HomeDetailFragment +import im.vector.riotx.features.home.HomeDrawerFragment +import im.vector.riotx.features.home.LoadingFragment +import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment +import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment +import im.vector.riotx.features.home.group.GroupListFragment +import im.vector.riotx.features.home.room.detail.RoomDetailFragment +import im.vector.riotx.features.home.room.list.RoomListFragment +import im.vector.riotx.features.login.LoginFragment +import im.vector.riotx.features.login.LoginSsoFallbackFragment +import im.vector.riotx.features.reactions.EmojiSearchResultFragment +import im.vector.riotx.features.roomdirectory.PublicRoomsFragment +import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment +import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment +import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment +import im.vector.riotx.features.settings.* +import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment +import im.vector.riotx.features.settings.push.PushGatewaysFragment + +@Module +interface FragmentModule { + + /** + * Fragments with @IntoMap will be injected by this factory + */ + @Binds + fun bindFragmentFactory(factory: VectorFragmentFactory): FragmentFactory + + @Binds + @IntoMap + @FragmentKey(RoomListFragment::class) + fun bindRoomListFragment(fragment: RoomListFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(GroupListFragment::class) + fun bindGroupListFragment(fragment: GroupListFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(RoomDetailFragment::class) + fun bindRoomDetailFragment(fragment: RoomDetailFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(RoomDirectoryPickerFragment::class) + fun bindRoomDirectoryPickerFragment(fragment: RoomDirectoryPickerFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(CreateRoomFragment::class) + fun bindCreateRoomFragment(fragment: CreateRoomFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(RoomPreviewNoPreviewFragment::class) + fun bindRoomPreviewNoPreviewFragment(fragment: RoomPreviewNoPreviewFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(KeysBackupSettingsFragment::class) + fun bindKeysBackupSettingsFragment(fragment: KeysBackupSettingsFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoadingFragment::class) + fun bindLoadingFragment(fragment: LoadingFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(HomeDrawerFragment::class) + fun bindHomeDrawerFragment(fragment: HomeDrawerFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(HomeDetailFragment::class) + fun bindHomeDetailFragment(fragment: HomeDetailFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(EmojiSearchResultFragment::class) + fun bindEmojiSearchResultFragment(fragment: EmojiSearchResultFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoginFragment::class) + fun bindLoginFragment(fragment: LoginFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(LoginSsoFallbackFragment::class) + fun bindLoginSsoFallbackFragment(fragment: LoginSsoFallbackFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(CreateDirectRoomDirectoryUsersFragment::class) + fun bindCreateDirectRoomDirectoryUsersFragment(fragment: CreateDirectRoomDirectoryUsersFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(CreateDirectRoomKnownUsersFragment::class) + fun bindCreateDirectRoomKnownUsersFragment(fragment: CreateDirectRoomKnownUsersFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(PushGatewaysFragment::class) + fun bindPushGatewaysFragment(fragment: PushGatewaysFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VectorSettingsNotificationsTroubleshootFragment::class) + fun bindVectorSettingsNotificationsTroubleshootFragment(fragment: VectorSettingsNotificationsTroubleshootFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VectorSettingsAdvancedNotificationPreferenceFragment::class) + fun bindVectorSettingsAdvancedNotificationPreferenceFragment(fragment: VectorSettingsAdvancedNotificationPreferenceFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VectorSettingsNotificationPreferenceFragment::class) + fun bindVectorSettingsNotificationPreferenceFragment(fragment: VectorSettingsNotificationPreferenceFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VectorSettingsPreferencesFragment::class) + fun bindVectorSettingsPreferencesFragment(fragment: VectorSettingsPreferencesFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VectorSettingsSecurityPrivacyFragment::class) + fun bindVectorSettingsSecurityPrivacyFragment(fragment: VectorSettingsSecurityPrivacyFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VectorSettingsHelpAboutFragment::class) + fun bindVectorSettingsHelpAboutFragment(fragment: VectorSettingsHelpAboutFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VectorSettingsIgnoredUsersFragment::class) + fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(SASVerificationIncomingFragment::class) + fun bindSASVerificationIncomingFragment(fragment: SASVerificationIncomingFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(SASVerificationShortCodeFragment::class) + fun bindSASVerificationShortCodeFragment(fragment: SASVerificationShortCodeFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(SASVerificationVerifiedFragment::class) + fun bindSASVerificationVerifiedFragment(fragment: SASVerificationVerifiedFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(SASVerificationStartFragment::class) + fun bindSASVerificationStartFragment(fragment: SASVerificationStartFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(PublicRoomsFragment::class) + fun bindPublicRoomsFragment(fragment: PublicRoomsFragment): Fragment +} diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 0efbc0e173..17622020d0 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -17,41 +17,26 @@ package im.vector.riotx.core.di import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentFactory import androidx.lifecycle.ViewModelProvider import dagger.BindsInstance import dagger.Component -import im.vector.fragments.keysbackup.restore.KeysBackupRestoreFromPassphraseFragment import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.features.MainActivity -import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyFragment -import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSuccessFragment import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity -import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment -import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupStep1Fragment -import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupStep2Fragment -import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupStep3Fragment -import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragment import im.vector.riotx.features.home.HomeActivity -import im.vector.riotx.features.home.HomeDetailFragment -import im.vector.riotx.features.home.HomeDrawerFragment import im.vector.riotx.features.home.HomeModule import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity -import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment -import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment -import im.vector.riotx.features.home.group.GroupListFragment -import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity -import im.vector.riotx.features.home.room.list.RoomListFragment +import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.riotx.features.home.room.list.RoomListModule import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.link.LinkHandlerActivity import im.vector.riotx.features.login.LoginActivity -import im.vector.riotx.features.login.LoginFragment -import im.vector.riotx.features.login.LoginSsoFallbackFragment import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.navigation.Navigator @@ -60,14 +45,9 @@ import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.reactions.widget.ReactionButton -import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity -import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment -import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment -import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment import im.vector.riotx.features.settings.* -import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.ui.UiStateRepository @@ -78,6 +58,7 @@ import im.vector.riotx.features.ui.UiStateRepository modules = [ AssistedInjectModule::class, ViewModelModule::class, + FragmentModule::class, HomeModule::class, RoomListModule::class ] @@ -87,6 +68,8 @@ interface ScreenComponent { fun activeSessionHolder(): ActiveSessionHolder + fun fragmentFactory(): FragmentFactory + fun viewModelFactory(): ViewModelProvider.Factory fun bugReporter(): BugReporter @@ -99,22 +82,6 @@ interface ScreenComponent { fun inject(activity: HomeActivity) - fun inject(roomDetailFragment: RoomDetailFragment) - - fun inject(roomListFragment: RoomListFragment) - - fun inject(groupListFragment: GroupListFragment) - - fun inject(roomDirectoryPickerFragment: RoomDirectoryPickerFragment) - - fun inject(roomPreviewNoPreviewFragment: RoomPreviewNoPreviewFragment) - - fun inject(keysBackupSettingsFragment: KeysBackupSettingsFragment) - - fun inject(homeDrawerFragment: HomeDrawerFragment) - - fun inject(homeDetailFragment: HomeDetailFragment) - fun inject(messageActionsBottomSheet: MessageActionsBottomSheet) fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet) @@ -123,30 +90,8 @@ interface ScreenComponent { fun inject(vectorSettingsActivity: VectorSettingsActivity) - fun inject(createRoomFragment: CreateRoomFragment) - fun inject(keysBackupManageActivity: KeysBackupManageActivity) - fun inject(keysBackupRestoreFromKeyFragment: KeysBackupRestoreFromKeyFragment) - - fun inject(keysBackupRestoreFromPassphraseFragment: KeysBackupRestoreFromPassphraseFragment) - - fun inject(keysBackupRestoreSuccessFragment: KeysBackupRestoreSuccessFragment) - - fun inject(keysBackupSetupStep1Fragment: KeysBackupSetupStep1Fragment) - - fun inject(keysBackupSetupStep2Fragment: KeysBackupSetupStep2Fragment) - - fun inject(keysBackupSetupStep3Fragment: KeysBackupSetupStep3Fragment) - - fun inject(publicRoomsFragment: PublicRoomsFragment) - - fun inject(loginFragment: LoginFragment) - - fun inject(loginSsoFallbackFragment: LoginSsoFallbackFragment) - - fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment) - fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity) fun inject(loginActivity: LoginActivity) @@ -169,26 +114,8 @@ interface ScreenComponent { fun inject(videoMediaViewerActivity: VideoMediaViewerActivity) - fun inject(vectorSettingsNotificationPreferenceFragment: VectorSettingsNotificationPreferenceFragment) - - fun inject(vectorSettingsPreferencesFragment: VectorSettingsPreferencesFragment) - - fun inject(vectorSettingsAdvancedNotificationPreferenceFragment: VectorSettingsAdvancedNotificationPreferenceFragment) - - fun inject(vectorSettingsSecurityPrivacyFragment: VectorSettingsSecurityPrivacyFragment) - - fun inject(vectorSettingsHelpAboutFragment: VectorSettingsHelpAboutFragment) - fun inject(userAvatarPreference: UserAvatarPreference) - fun inject(vectorSettingsNotificationsTroubleshootFragment: VectorSettingsNotificationsTroubleshootFragment) - - fun inject(pushGatewaysFragment: PushGatewaysFragment) - - fun inject(createDirectRoomKnownUsersFragment: CreateDirectRoomKnownUsersFragment) - - fun inject(createDirectRoomDirectoryUsersFragment: CreateDirectRoomDirectoryUsersFragment) - fun inject(createDirectRoomActivity: CreateDirectRoomActivity) fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet) @@ -197,6 +124,8 @@ interface ScreenComponent { fun inject(incomingShareActivity: IncomingShareActivity) + fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet) + @Component.Factory interface Factory { fun create(vectorComponent: VectorComponent, 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 2dfbb5f799..d31955ce8e 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 @@ -23,7 +23,7 @@ import dagger.Component import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.session.Session -import im.vector.riotx.ActiveSessionObservableStore +import im.vector.riotx.ActiveSessionDataSource import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.VectorApplication @@ -33,8 +33,8 @@ import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.HomeRoomListObservableStore -import im.vector.riotx.features.home.group.SelectedGroupStore +import im.vector.riotx.features.home.HomeRoomListDataSource +import im.vector.riotx.features.home.group.SelectedGroupDataSource import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.notifications.* @@ -43,7 +43,7 @@ import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.settings.VectorPreferences -import im.vector.riotx.features.share.ShareRoomListObservableStore +import im.vector.riotx.features.share.ShareRoomListDataSource import im.vector.riotx.features.ui.UiStateRepository import javax.inject.Singleton @@ -85,13 +85,13 @@ interface VectorComponent { fun navigator(): Navigator - fun homeRoomListObservableStore(): HomeRoomListObservableStore + fun homeRoomListObservableStore(): HomeRoomListDataSource - fun shareRoomListObservableStore(): ShareRoomListObservableStore + fun shareRoomListObservableStore(): ShareRoomListDataSource - fun selectedGroupStore(): SelectedGroupStore + fun selectedGroupStore(): SelectedGroupDataSource - fun activeSessionObservableStore(): ActiveSessionObservableStore + fun activeSessionObservableStore(): ActiveSessionDataSource fun incomingVerificationRequestHandler(): IncomingVerificationRequestHandler diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorFragmentFactory.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorFragmentFactory.kt new file mode 100644 index 0000000000..6b33b9e2a7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorFragmentFactory.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.core.di + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Provider + +/** + * FragmentFactory which uses Dagger to create the instances. + */ +class VectorFragmentFactory @Inject constructor( + private val creators: @JvmSuppressWildcards Map, Provider> +) : FragmentFactory() { + + override fun instantiate(classLoader: ClassLoader, className: String): Fragment { + val fragmentClass = loadFragmentClass(classLoader, className) + val creator: Provider? = creators[fragmentClass] + return if (creator == null) { + Timber.v("Unknown model class: $className, fallback to default instance") + super.instantiate(classLoader, className) + } else { + creator.get() + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 98c89c421a..cc1e4dabc7 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -27,10 +27,12 @@ import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromP import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.riotx.features.crypto.verification.SasVerificationViewModel -import im.vector.riotx.features.home.HomeNavigationViewModel -import im.vector.riotx.features.home.createdirect.CreateDirectRoomNavigationViewModel +import im.vector.riotx.features.home.HomeSharedActionViewModel +import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel +import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel +import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.riotx.features.reactions.EmojiChooserViewModel -import im.vector.riotx.features.roomdirectory.RoomDirectoryNavigationViewModel +import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel @Module @@ -76,16 +78,6 @@ interface ViewModelModule { @ViewModelKey(KeysBackupRestoreFromPassphraseViewModel::class) fun bindKeysBackupRestoreFromPassphraseViewModel(viewModel: KeysBackupRestoreFromPassphraseViewModel): ViewModel - @Binds - @IntoMap - @ViewModelKey(RoomDirectoryNavigationViewModel::class) - fun bindRoomDirectoryNavigationViewModel(viewModel: RoomDirectoryNavigationViewModel): ViewModel - - @Binds - @IntoMap - @ViewModelKey(HomeNavigationViewModel::class) - fun bindHomeNavigationViewModel(viewModel: HomeNavigationViewModel): ViewModel - @Binds @IntoMap @ViewModelKey(KeysBackupSetupSharedViewModel::class) @@ -98,6 +90,26 @@ interface ViewModelModule { @Binds @IntoMap - @ViewModelKey(CreateDirectRoomNavigationViewModel::class) - fun bindCreateDirectRoomNavigationViewModel(viewModel: CreateDirectRoomNavigationViewModel): ViewModel + @ViewModelKey(CreateDirectRoomSharedActionViewModel::class) + fun bindCreateDirectRoomSharedActionViewModel(viewModel: CreateDirectRoomSharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(HomeSharedActionViewModel::class) + fun bindHomeSharedActionViewModel(viewModel: HomeSharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(MessageSharedActionViewModel::class) + fun bindMessageSharedActionViewModel(viewModel: MessageSharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RoomListQuickActionsSharedActionViewModel::class) + fun bindRoomListQuickActionsSharedActionViewModel(viewModel: RoomListQuickActionsSharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RoomDirectorySharedActionViewModel::class) + fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel } diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt index fb320afded..1cb6c5406a 100644 --- a/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/ExportKeysDialog.kt @@ -44,15 +44,15 @@ class ExportKeysDialog { val textWatcher = object : SimpleTextWatcher() { override fun afterTextChanged(s: Editable) { when { - passPhrase1EditText.text.isNullOrEmpty() -> { + passPhrase1EditText.text.isNullOrEmpty() -> { exportButton.isEnabled = false passPhrase2Til.error = null } - passPhrase1EditText.text == passPhrase2EditText.text -> { + passPhrase1EditText.text.toString() == passPhrase2EditText.text.toString() -> { exportButton.isEnabled = true passPhrase2Til.error = null } - else -> { + else -> { exportButton.isEnabled = false passPhrase2Til.error = activity.getString(R.string.passphrase_passphrase_does_not_match) } diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/HelpFooterItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/HelpFooterItem.kt new file mode 100644 index 0000000000..2784db5fce --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/HelpFooterItem.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.epoxy + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R + +@EpoxyModelClass(layout = R.layout.item_help_footer) +abstract class HelpFooterItem : VectorEpoxyModel() { + + @EpoxyAttribute + var text: String? = null + + override fun bind(holder: Holder) { + holder.textView.text = text + } + + class Holder : VectorEpoxyHolder() { + val textView by bind(R.id.itemHelpText) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt similarity index 51% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt index d0d5b1deea..483650a434 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemAction.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemAction.kt @@ -5,26 +5,33 @@ * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.core.epoxy.bottomsheet +import android.content.res.ColorStateList import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.core.widget.ImageViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.features.themes.ThemeUtils /** * A action for bottom sheet. @@ -42,28 +49,48 @@ abstract class BottomSheetItemAction : VectorEpoxyModel(R.id.action_start_space) - val icon by bind(R.id.action_icon) - val text by bind(R.id.action_title) - val expand by bind(R.id.action_expand) + val startSpace by bind(R.id.actionStartSpace) + val icon by bind(R.id.actionIcon) + val text by bind(R.id.actionTitle) + val selected by bind(R.id.actionSelected) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt similarity index 84% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt index d37aa43770..999068b289 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemMessagePreview.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemMessagePreview.kt @@ -5,15 +5,16 @@ * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.core.epoxy.bottomsheet import android.widget.ImageView import android.widget.TextView @@ -24,7 +25,6 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData /** * A message preview for bottom sheet. @@ -35,7 +35,9 @@ abstract class BottomSheetItemMessagePreview : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute + lateinit var avatarUrl: String + @EpoxyAttribute + lateinit var roomId: String + @EpoxyAttribute + var roomName: String? = null + @EpoxyAttribute var settingsClickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + avatarRenderer.render(avatarUrl, roomId, roomName, holder.avatar) + holder.roomName.setTextOrHide(roomName) + holder.roomSettings.setOnClickListener(settingsClickListener) + } + + class Holder : VectorEpoxyHolder() { + val avatar by bind(R.id.bottomSheetRoomPreviewAvatar) + val roomName by bind(R.id.bottomSheetRoomPreviewName) + val roomSettings by bind(R.id.bottomSheetRoomPreviewSettings) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt similarity index 93% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt index 86a5512349..08d727cfa9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSendState.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSendState.kt @@ -5,15 +5,16 @@ * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.core.epoxy.bottomsheet import android.view.View import android.widget.TextView diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt similarity index 88% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt index f09f68b714..fddf507bf9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/BottomSheetItemSeparator.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetItemSeparator.kt @@ -5,15 +5,16 @@ * 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ -package im.vector.riotx.features.home.room.detail.timeline.action +package im.vector.riotx.core.epoxy.bottomsheet import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt index b4afb569c4..6d7c3d39e6 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt @@ -16,21 +16,40 @@ package im.vector.riotx.core.extensions -import androidx.appcompat.app.AppCompatActivity +import android.os.Parcelable import androidx.fragment.app.Fragment +import im.vector.riotx.core.platform.VectorBaseActivity -fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int) { - supportFragmentManager.inTransaction { add(frameId, fragment) } +fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) { + supportFragmentManager.commitTransactionNow { add(frameId, fragment) } } -fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int, tag: String? = null) { - supportFragmentManager.inTransaction { replace(frameId, fragment, tag) } +fun VectorBaseActivity.addFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { + supportFragmentManager.commitTransactionNow { + add(frameId, fragmentClass, params.toMvRxBundle(), tag) + } } -fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) { - supportFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) } +fun VectorBaseActivity.replaceFragment(frameId: Int, fragment: Fragment, tag: String? = null) { + supportFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) } } -fun AppCompatActivity.hideKeyboard() { +fun VectorBaseActivity.replaceFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { + supportFragmentManager.commitTransactionNow { + replace(frameId, fragmentClass, params.toMvRxBundle(), tag) + } +} + +fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragment: Fragment, tag: String? = null) { + supportFragmentManager.commitTransaction { replace(frameId, fragment).addToBackStack(tag) } +} + +fun VectorBaseActivity.addFragmentToBackstack(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { + supportFragmentManager.commitTransaction { + replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag) + } +} + +fun VectorBaseActivity.hideKeyboard() { currentFocus?.hideKeyboard() } diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt index 65f8fb2aaa..7db27ececb 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt @@ -16,28 +16,66 @@ package im.vector.riotx.core.extensions +import android.os.Parcelable import androidx.fragment.app.Fragment +import im.vector.riotx.core.platform.VectorBaseFragment -fun Fragment.addFragment(fragment: Fragment, frameId: Int) { - fragmentManager?.inTransaction { add(frameId, fragment) } +fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) { + parentFragmentManager.commitTransactionNow { add(frameId, fragment) } } -fun Fragment.replaceFragment(fragment: Fragment, frameId: Int) { - fragmentManager?.inTransaction { replace(frameId, fragment) } +fun VectorBaseFragment.addFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { + parentFragmentManager.commitTransactionNow { + add(frameId, fragmentClass, params.toMvRxBundle(), tag) + } } -fun Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) { - fragmentManager?.inTransaction { replace(frameId, fragment).addToBackStack(tag) } +fun VectorBaseFragment.replaceFragment(frameId: Int, fragment: Fragment) { + parentFragmentManager.commitTransactionNow { replace(frameId, fragment) } } -fun Fragment.addChildFragment(fragment: Fragment, frameId: Int) { - childFragmentManager.inTransaction { add(frameId, fragment) } +fun VectorBaseFragment.replaceFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { + parentFragmentManager.commitTransactionNow { + replace(frameId, fragmentClass, params.toMvRxBundle(), tag) + } } -fun Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) { - childFragmentManager.inTransaction { replace(frameId, fragment) } +fun VectorBaseFragment.addFragmentToBackstack(frameId: Int, fragment: Fragment, tag: String? = null) { + parentFragmentManager.commitTransaction { replace(frameId, fragment, tag).addToBackStack(tag) } } -fun Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) { - childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) } +fun VectorBaseFragment.addFragmentToBackstack(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { + parentFragmentManager.commitTransaction { + replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag) + } +} + +fun VectorBaseFragment.addChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) { + childFragmentManager.commitTransactionNow { add(frameId, fragment, tag) } +} + +fun VectorBaseFragment.addChildFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { + childFragmentManager.commitTransactionNow { + add(frameId, fragmentClass, params.toMvRxBundle(), tag) + } +} + +fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) { + childFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) } +} + +fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { + childFragmentManager.commitTransactionNow { + replace(frameId, fragmentClass, params.toMvRxBundle(), tag) + } +} + +fun VectorBaseFragment.addChildFragmentToBackstack(frameId: Int, fragment: Fragment, tag: String? = null) { + childFragmentManager.commitTransaction { replace(frameId, fragment).addToBackStack(tag) } +} + +fun VectorBaseFragment.addChildFragmentToBackstack(frameId: Int, fragmentClass: Class, params: Parcelable? = null, tag: String? = null) { + childFragmentManager.commitTransaction { + replace(frameId, fragmentClass, params.toMvRxBundle(), tag).addToBackStack(tag) + } } diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/FragmentManager.kt b/vector/src/main/java/im/vector/riotx/core/extensions/FragmentManager.kt index 1d7815b2b7..caf1bf90f8 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/FragmentManager.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/FragmentManager.kt @@ -18,6 +18,10 @@ package im.vector.riotx.core.extensions import androidx.fragment.app.FragmentTransaction -inline fun androidx.fragment.app.FragmentManager.inTransaction(func: FragmentTransaction.() -> FragmentTransaction) { +inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: FragmentTransaction.() -> FragmentTransaction) { + beginTransaction().func().commitNow() +} + +inline fun androidx.fragment.app.FragmentManager.commitTransaction(func: FragmentTransaction.() -> FragmentTransaction) { beginTransaction().func().commit() } diff --git a/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt b/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt index c1f58306a4..677f7894e8 100644 --- a/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt +++ b/vector/src/main/java/im/vector/riotx/core/files/FileSaver.kt @@ -30,15 +30,10 @@ import java.io.File */ @WorkerThread fun writeToFile(str: String, file: File): Try { - return Try { - val sink = file.sink() - - val bufferedSink = sink.buffer() - - bufferedSink.writeString(str, Charsets.UTF_8) - - bufferedSink.close() - sink.close() + return Try { + file.sink().buffer().use { + it.writeString(str, Charsets.UTF_8) + } } } @@ -47,15 +42,10 @@ fun writeToFile(str: String, file: File): Try { */ @WorkerThread fun writeToFile(data: ByteArray, file: File): Try { - return Try { - val sink = file.sink() - - val bufferedSink = sink.buffer() - - bufferedSink.write(data) - - bufferedSink.close() - sink.close() + return Try { + file.sink().buffer().use { + it.write(data) + } } } diff --git a/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt b/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt index b6ae2be20b..84cba7392f 100644 --- a/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt +++ b/vector/src/main/java/im/vector/riotx/core/images/ImageTools.kt @@ -17,7 +17,6 @@ package im.vector.riotx.core.images import android.content.Context -import android.database.Cursor import android.net.Uri import android.provider.MediaStore import androidx.exifinterface.media.ExifInterface @@ -37,26 +36,24 @@ class ImageTools @Inject constructor(private val context: Context) { if (uri.scheme == "content") { val proj = arrayOf(MediaStore.Images.Media.DATA) - var cursor: Cursor? = null try { - cursor = context.contentResolver.query(uri, proj, null, null, null) - if (cursor != null && cursor.count > 0) { - cursor.moveToFirst() - val idxData = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) - val path = cursor.getString(idxData) - if (path.isNullOrBlank()) { - Timber.w("Cannot find path in media db for uri $uri") - return orientation + val cursor = context.contentResolver.query(uri, proj, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val idxData = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) + val path = it.getString(idxData) + if (path.isNullOrBlank()) { + Timber.w("Cannot find path in media db for uri $uri") + return orientation + } + val exif = ExifInterface(path) + orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) } - val exif = ExifInterface(path) - orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) } } catch (e: Exception) { // eg SecurityException from com.google.android.apps.photos.content.GooglePhotosImageProvider URIs // eg IOException from trying to parse the returned path as a file when it is an http uri. Timber.e(e, "Cannot get orientation for bitmap") - } finally { - cursor?.close() } } else if (uri.scheme == "file") { try { diff --git a/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt b/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt index 9e9f0ae508..2b6740f62f 100644 --- a/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt +++ b/vector/src/main/java/im/vector/riotx/core/intent/Filename.kt @@ -17,28 +17,17 @@ package im.vector.riotx.core.intent import android.content.Context -import android.database.Cursor import android.net.Uri import android.provider.OpenableColumns fun getFilenameFromUri(context: Context?, uri: Uri): String? { - var result: String? = null if (context != null && uri.scheme == "content") { - val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null) - try { - if (cursor != null && cursor.moveToFirst()) { - result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + val cursor = context.contentResolver.query(uri, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + return it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) } - } finally { - cursor?.close() } } - if (result == null) { - result = uri.path - val cut = result?.lastIndexOf('/') ?: -1 - if (cut != -1) { - result = result?.substring(cut + 1) - } - } - return result + return uri.path?.substringAfterLast('/') } diff --git a/vector/src/main/java/im/vector/riotx/core/linkify/VectorAutoLinkPatterns.kt b/vector/src/main/java/im/vector/riotx/core/linkify/VectorAutoLinkPatterns.kt index e6eb886e02..ae4131b5e9 100644 --- a/vector/src/main/java/im/vector/riotx/core/linkify/VectorAutoLinkPatterns.kt +++ b/vector/src/main/java/im/vector/riotx/core/linkify/VectorAutoLinkPatterns.kt @@ -15,8 +15,6 @@ */ package im.vector.riotx.core.linkify -import java.util.regex.Pattern - /** * Better support for geo URi */ @@ -26,7 +24,7 @@ object VectorAutoLinkPatterns { private const val LAT_OR_LONG_OR_ALT_NUMBER = "-?\\d+(?:\\.\\d+)?" private const val COORDINATE_SYSTEM = ";crs=[\\w-]+" - val GEO_URI: Pattern = Pattern.compile("(?:geo:)?" + + val GEO_URI: Regex = Regex("(?:geo:)?" + "(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" + "," + "(" + LAT_OR_LONG_OR_ALT_NUMBER + ")" + @@ -35,5 +33,5 @@ object VectorAutoLinkPatterns { "(?:" + ";u=\\d+(?:\\.\\d+)?" + ")?" + // uncertainty in meters "(?:" + ";[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+" + // dafuk - ")*", Pattern.CASE_INSENSITIVE) + ")*", RegexOption.IGNORE_CASE) } diff --git a/vector/src/main/java/im/vector/riotx/core/linkify/VectorLinkify.kt b/vector/src/main/java/im/vector/riotx/core/linkify/VectorLinkify.kt index 358fff6092..99b0316cbe 100644 --- a/vector/src/main/java/im/vector/riotx/core/linkify/VectorLinkify.kt +++ b/vector/src/main/java/im/vector/riotx/core/linkify/VectorLinkify.kt @@ -19,7 +19,6 @@ import android.text.Spannable import android.text.style.URLSpan import android.text.util.Linkify import androidx.core.text.util.LinkifyCompat -import java.util.* object VectorLinkify { /** @@ -95,7 +94,7 @@ object VectorLinkify { createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end)) } - LinkifyCompat.addLinks(spannable, VectorAutoLinkPatterns.GEO_URI, "geo:", arrayOf("geo:"), geoMatchFilter, null) + LinkifyCompat.addLinks(spannable, VectorAutoLinkPatterns.GEO_URI.toPattern(), "geo:", arrayOf("geo:"), geoMatchFilter, null) spannable.forEachSpanIndexed { _, urlSpan, start, end -> spannable.removeSpan(urlSpan) createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end)) @@ -108,7 +107,7 @@ object VectorLinkify { } private fun pruneOverlaps(links: ArrayList) { - Collections.sort(links, COMPARATOR) + links.sortWith(COMPARATOR) var len = links.size var i = 0 while (i < len - 1) { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt index fc09ad0f75..b8587750a3 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt @@ -16,17 +16,15 @@ package im.vector.riotx.core.platform -import android.annotation.TargetApi import android.content.Context -import android.os.Build import android.util.AttributeSet -import android.widget.ScrollView - +import androidx.core.widget.NestedScrollView import im.vector.riotx.R private const val DEFAULT_MAX_HEIGHT = 200 -class MaxHeightScrollView : ScrollView { +class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) + : NestedScrollView(context, attrs, defStyle) { var maxHeight: Int = 0 set(value) { @@ -34,28 +32,7 @@ class MaxHeightScrollView : ScrollView { requestLayout() } - constructor(context: Context) : super(context) {} - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - if (!isInEditMode) { - init(context, attrs) - } - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - if (!isInEditMode) { - init(context, attrs) - } - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { - if (!isInEditMode) { - init(context, attrs) - } - } - - private fun init(context: Context, attrs: AttributeSet?) { + init { if (attrs != null) { val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView) maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 9a4f89d13a..4a3056657f 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -19,10 +19,12 @@ package im.vector.riotx.core.platform import android.content.Context import android.content.res.Configuration import android.os.Bundle +import android.os.Parcelable import android.view.Menu import android.view.MenuItem import android.view.View import androidx.annotation.* +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible @@ -33,7 +35,7 @@ import androidx.lifecycle.ViewModelProviders import butterknife.BindView import butterknife.ButterKnife import butterknife.Unbinder -import com.airbnb.mvrx.BaseMvRxActivity +import com.airbnb.mvrx.MvRx import com.bumptech.glide.util.Util import com.google.android.material.snackbar.Snackbar import im.vector.riotx.BuildConfig @@ -57,7 +59,7 @@ import io.reactivex.disposables.Disposable import timber.log.Timber import kotlin.system.measureTimeMillis -abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { +abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { /* ========================================================================================== * UI * ========================================================================================== */ @@ -67,11 +69,19 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { @BindView(R.id.vector_coordinator_layout) var coordinatorLayout: CoordinatorLayout? = null + /* ========================================================================================== + * View model + * ========================================================================================== */ + + private lateinit var viewModelFactory: ViewModelProvider.Factory + + protected val viewModelProvider + get() = ViewModelProviders.of(this, viewModelFactory) + /* ========================================================================================== * DATA * ========================================================================================== */ - protected lateinit var viewModelFactory: ViewModelProvider.Factory private lateinit var configurationViewModel: ConfigurationViewModel private lateinit var sessionListener: SessionListener protected lateinit var bugReporter: BugReporter @@ -125,10 +135,10 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { } Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms") ThemeUtils.setActivityTheme(this, getOtherThemes()) - + supportFragmentManager.fragmentFactory = screenComponent.fragmentFactory() super.onCreate(savedInstanceState) viewModelFactory = screenComponent.viewModelFactory() - configurationViewModel = ViewModelProviders.of(this, viewModelFactory).get(ConfigurationViewModel::class.java) + configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java) bugReporter = screenComponent.bugReporter() // Shake detector rageShake = screenComponent.rageShake() @@ -331,6 +341,10 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector { } } + fun Parcelable?.toMvRxBundle(): Bundle? { + return this?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } + } + // ============================================================================================== // Handle loading view (also called waiting view or spinner view) // ============================================================================================== diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt index 8d40d55a7a..5727580653 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -21,26 +21,42 @@ import android.os.Bundle import android.os.Parcelable import android.widget.FrameLayout import androidx.annotation.CallSuper +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRxView -import com.airbnb.mvrx.MvRxViewModelStore +import com.airbnb.mvrx.MvRxViewId import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.utils.DimensionConverter -import java.util.* /** * Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment) */ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment(), MvRxView { - override val mvrxViewModelStore by lazy { MvRxViewModelStore(viewModelStore) } - private lateinit var mvrxPersistedViewId: String + private val mvrxViewIdProperty = MvRxViewId() + final override val mvrxViewId: String by mvrxViewIdProperty private lateinit var screenComponent: ScreenComponent - final override val mvrxViewId: String by lazy { mvrxPersistedViewId } + + /* ========================================================================================== + * View model + * ========================================================================================== */ + + private lateinit var viewModelFactory: ViewModelProvider.Factory + + protected val activityViewModelProvider + get() = ViewModelProviders.of(requireActivity(), viewModelFactory) + + protected val fragmentViewModelProvider + get() = ViewModelProviders.of(this, viewModelFactory) + + /* ========================================================================================== + * BottomSheetBehavior + * ========================================================================================== */ private var bottomSheetBehavior: BottomSheetBehavior? = null @@ -52,6 +68,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() override fun onAttach(context: Context) { screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) + viewModelFactory = screenComponent.viewModelFactory() super.onAttach(context) injectWith(screenComponent) } @@ -59,10 +76,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() protected open fun injectWith(screenComponent: ScreenComponent) = Unit override fun onCreate(savedInstanceState: Bundle?) { - mvrxViewModelStore.restoreViewModels(this, savedInstanceState) - mvrxPersistedViewId = savedInstanceState?.getString(PERSISTED_VIEW_ID_KEY) - ?: this::class.java.simpleName + "_" + UUID.randomUUID().toString() - + mvrxViewIdProperty.restoreFrom(savedInstanceState) super.onCreate(savedInstanceState) } @@ -79,8 +93,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - mvrxViewModelStore.saveViewModels(outState) - outState.putString(PERSISTED_VIEW_ID_KEY, mvrxViewId) + mvrxViewIdProperty.saveTo(outState) } override fun onStart() { @@ -102,5 +115,3 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } } } - -private const val PERSISTED_VIEW_ID_KEY = "mvrx:bottomsheet_persisted_view_id" diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index cc0d8940bc..9f94c15edd 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -25,6 +25,7 @@ import androidx.annotation.LayoutRes import androidx.annotation.MainThread import androidx.appcompat.widget.Toolbar import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders import butterknife.ButterKnife import butterknife.Unbinder import com.airbnb.mvrx.BaseMvRxFragment @@ -51,10 +52,21 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { * Navigator * ========================================================================================== */ - protected lateinit var viewModelFactory: ViewModelProvider.Factory protected lateinit var navigator: Navigator private lateinit var screenComponent: ScreenComponent + /* ========================================================================================== + * View model + * ========================================================================================== */ + + private lateinit var viewModelFactory: ViewModelProvider.Factory + + protected val activityViewModelProvider + get() = ViewModelProviders.of(requireActivity(), viewModelFactory) + + protected val fragmentViewModelProvider + get() = ViewModelProviders.of(this, viewModelFactory) + /* ========================================================================================== * Life cycle * ========================================================================================== */ @@ -63,6 +75,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) navigator = screenComponent.navigator() viewModelFactory = screenComponent.viewModelFactory() + childFragmentManager.fragmentFactory = screenComponent.fragmentFactory() injectWith(injector()) super.onAttach(context) } @@ -101,11 +114,12 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { super.onDestroyView() mUnBinder?.unbind() mUnBinder = null + uiDisposables.clear() } override fun onDestroy() { - super.onDestroy() uiDisposables.dispose() + super.onDestroy() } override fun injector(): ScreenComponent { @@ -134,7 +148,11 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { } protected fun setArguments(args: Parcelable? = null) { - arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } + arguments = args.toMvRxBundle() + } + + fun Parcelable?.toMvRxBundle(): Bundle? { + return this?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } } @MainThread @@ -164,7 +182,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { private val uiDisposables = CompositeDisposable() - protected fun Disposable.disposeOnDestroy(): Disposable { + protected fun Disposable.disposeOnDestroyView(): Disposable { uiDisposables.add(this) return this } diff --git a/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorEventViewModel.kt similarity index 54% rename from vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt rename to vector/src/main/java/im/vector/riotx/core/platform/VectorEventViewModel.kt index 3491f8d340..e928be3343 100644 --- a/vector/src/main/java/im/vector/riotx/core/mvrx/NavigationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorEventViewModel.kt @@ -14,21 +14,16 @@ * limitations under the License. */ -package im.vector.riotx.core.mvrx +package im.vector.riotx.core.platform -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import im.vector.riotx.core.extensions.postLiveEvent -import im.vector.riotx.core.utils.LiveEvent +import im.vector.riotx.core.utils.MutableDataSource +import im.vector.riotx.core.utils.PublishDataSource -abstract class NavigationViewModel : ViewModel() { +interface VectorSharedAction - private val _navigateTo = MutableLiveData>() - val navigateTo: LiveData> - get() = _navigateTo - - fun goTo(navigation: NavigationClass) { - _navigateTo.postLiveEvent(navigation) - } -} +/** + * Parent class to handle navigation events, action events, or other any events + */ +open class VectorSharedActionViewModel(private val store: MutableDataSource = PublishDataSource()) + : ViewModel(), MutableDataSource by store diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorPreferenceFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorPreferenceFragment.kt deleted file mode 100644 index d534cd2297..0000000000 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorPreferenceFragment.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.core.platform - -import androidx.annotation.CallSuper -import androidx.preference.PreferenceFragmentCompat -import im.vector.riotx.R -import im.vector.riotx.core.utils.toast -import timber.log.Timber - -abstract class VectorPreferenceFragment : PreferenceFragmentCompat() { - - val vectorActivity: VectorBaseActivity by lazy { - activity as VectorBaseActivity - } - - abstract var titleRes: Int - - /* ========================================================================================== - * Life cycle - * ========================================================================================== */ - - @CallSuper - override fun onResume() { - super.onResume() - - (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(titleRes) - Timber.v("onResume Fragment ${this.javaClass.simpleName}") - } - - /* ========================================================================================== - * Protected - * ========================================================================================== */ - - protected fun notImplemented() { - // Snackbar cannot be display on PreferenceFragment - // Snackbar.make(view!!, R.string.not_implemented, Snackbar.LENGTH_SHORT) - activity?.toast(R.string.not_implemented) - } -} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt index 9679c20efb..74b18be3c2 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt @@ -16,13 +16,21 @@ package im.vector.riotx.core.platform +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.* +import im.vector.riotx.core.utils.LiveEvent import io.reactivex.Observable import io.reactivex.Single -abstract class VectorViewModel(initialState: S) +abstract class VectorViewModel(initialState: S) : BaseMvRxViewModel(initialState, false) { + // Generic handling of any request error + protected val _requestErrorLiveData = MutableLiveData>() + val requestErrorLiveData: LiveData> + get() = _requestErrorLiveData + /** * This method does the same thing as the execute function, but it doesn't subscribe to the stream * so you can use this in a switchMap or a flatMap @@ -44,4 +52,6 @@ abstract class VectorViewModel(initialState: S) .onErrorReturn { Fail(it) } .doOnNext { setState { stateReducer(it) } } } + + abstract fun handle(action: A) } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModelAction.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModelAction.kt new file mode 100644 index 0000000000..81c1996497 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModelAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.platform + +interface VectorViewModelAction + +/** + * To use when no action is associated to the ViewModel + */ +object EmptyAction : VectorViewModelAction diff --git a/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt index 41287d4e38..e2c08a1fe8 100644 --- a/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt @@ -22,8 +22,9 @@ import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.resources.AppNameProvider import im.vector.riotx.core.resources.LocaleProvider import im.vector.riotx.core.resources.StringProvider -import java.util.* +import java.util.UUID import javax.inject.Inject +import kotlin.math.abs private const val DEFAULT_PUSHER_FILE_TAG = "mobile" @@ -36,7 +37,7 @@ class PushersManager @Inject constructor( fun registerPusherWithFcmKey(pushKey: String): UUID { val currentSession = activeSessionHolder.getActiveSession() - var profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + Math.abs(currentSession.myUserId.hashCode()) + val profileTag = DEFAULT_PUSHER_FILE_TAG + "_" + abs(currentSession.myUserId.hashCode()) return currentSession.addHttpPusher( pushKey, diff --git a/vector/src/main/java/im/vector/riotx/core/resources/ColorProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/ColorProvider.kt index aa5fba839b..d19354240c 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/ColorProvider.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/ColorProvider.kt @@ -16,15 +16,15 @@ package im.vector.riotx.core.resources +import android.content.Context import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.ColorRes -import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import im.vector.riotx.features.themes.ThemeUtils import javax.inject.Inject -class ColorProvider @Inject constructor(private val context: AppCompatActivity) { +class ColorProvider @Inject constructor(private val context: Context) { fun getColor(@ColorRes colorRes: Int): Int { return ContextCompat.getColor(context, colorRes) @@ -33,7 +33,6 @@ class ColorProvider @Inject constructor(private val context: AppCompatActivity) /** * Translates color attributes to colors * - * @param c Context * @param colorAttribute Color Attribute * @return Requested Color */ diff --git a/vector/src/main/java/im/vector/riotx/core/resources/LocaleProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/LocaleProvider.kt index 74861a65cc..c78a5a99b8 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/LocaleProvider.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/LocaleProvider.kt @@ -18,7 +18,7 @@ package im.vector.riotx.core.resources import android.content.res.Resources import androidx.core.os.ConfigurationCompat -import java.util.* +import java.util.Locale import javax.inject.Inject class LocaleProvider @Inject constructor(private val resources: Resources) { diff --git a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt index bf3e360699..ac379a8f98 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt @@ -28,4 +28,12 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: fun shouldShowReadReceipts(): Boolean { return vectorPreferences.showReadReceipts() } + + fun shouldShowLongClickOnRoomHelp(): Boolean { + return vectorPreferences.shouldShowLongClickOnRoomHelp() + } + + fun neverShowLongClickOnRoomHelpAgain() { + vectorPreferences.neverShowLongClickOnRoomHelpAgain() + } } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt index c74e9a4111..6e4229908f 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt @@ -23,6 +23,7 @@ import android.widget.ImageView import android.widget.LinearLayout import androidx.core.view.isVisible import im.vector.riotx.R +import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import kotlinx.android.synthetic.main.view_read_receipts.view.* @@ -105,4 +106,11 @@ class ReadReceiptsView @JvmOverloads constructor( isVisible = false } } + + fun unbind() { + receiptAvatars.forEach { + GlideApp.with(context.applicationContext).clear(it) + } + isVisible = false + } } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt new file mode 100644 index 0000000000..726d2ea697 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.utils + +import com.jakewharton.rxrelay2.BehaviorRelay +import com.jakewharton.rxrelay2.PublishRelay +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers + +interface DataSource { + fun observe(): Observable +} + +interface MutableDataSource : DataSource { + fun post(value: T) +} + +/** + * This datasource emits the most recent value it has observed and all subsequent observed values to each subscriber. + */ +open class BehaviorDataSource(private val defaultValue: T? = null) : MutableDataSource { + + private val behaviorRelay = createRelay() + + override fun observe(): Observable { + return behaviorRelay.hide().observeOn(Schedulers.computation()) + } + + override fun post(value: T) { + behaviorRelay.accept(value) + } + + private fun createRelay(): BehaviorRelay { + return if (defaultValue == null) { + BehaviorRelay.create() + } else { + BehaviorRelay.createDefault(defaultValue) + } + } +} + +/** + * This datasource only emits all subsequent observed values to each subscriber. + */ +open class PublishDataSource : MutableDataSource { + + private val publishRelay = PublishRelay.create() + + override fun observe(): Observable { + return publishRelay.hide() + } + + override fun post(value: T) { + publishRelay.accept(value) + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DebouncedClickListener.kt b/vector/src/main/java/im/vector/riotx/core/utils/DebouncedClickListener.kt index 958f642565..230b11f14d 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/DebouncedClickListener.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/DebouncedClickListener.kt @@ -16,7 +16,7 @@ package im.vector.riotx.core.utils import android.view.View -import java.util.* +import java.util.WeakHashMap /** * Simple Debounced OnClickListener diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt b/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt index c65fcafb16..a5babcc885 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt @@ -59,9 +59,10 @@ fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) { val jsonAdapter = moshi.adapter(EmojiDataSource.EmojiData::class.java) val inputAsString = input.bufferedReader().use { it.readText() } val source = jsonAdapter.fromJson(inputAsString) - knownEmojiSet = HashSet() - source?.emojis?.values?.forEach { - knownEmojiSet?.add(it.emojiString()) + knownEmojiSet = HashSet().also { + source?.emojis?.mapTo(it) { (_, value) -> + value.emojiString() + } } done?.invoke() } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt index 9572b07216..78242d58de 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt @@ -32,7 +32,8 @@ import im.vector.riotx.R import timber.log.Timber import java.io.File import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale /** * Open a url in the internet browser of the system diff --git a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt index 8f97ef0247..f8cdeb3de6 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/PermissionsTools.kt @@ -67,6 +67,7 @@ const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573 const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574 const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575 const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576 +const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577 /** * Log the used permissions statuses. diff --git a/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt b/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt deleted file mode 100644 index b539ade931..0000000000 --- a/vector/src/main/java/im/vector/riotx/core/utils/RxStore.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.core.utils - -import com.jakewharton.rxrelay2.BehaviorRelay -import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers - -open class RxStore(private val defaultValue: T? = null) { - - private val storeRelay = createRelay() - - fun observe(): Observable { - return storeRelay.hide().observeOn(Schedulers.computation()) - } - - fun post(value: T) { - storeRelay.accept(value) - } - - private fun createRelay(): BehaviorRelay { - return if (defaultValue == null) { - BehaviorRelay.create() - } else { - BehaviorRelay.createDefault(defaultValue) - } - } -} 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 12371fe72d..ba0b99762b 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 @@ -31,7 +31,7 @@ import im.vector.riotx.R import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.settings.VectorLocale import timber.log.Timber -import java.util.* +import java.util.Locale /** * Tells if the application ignores battery optimizations. diff --git a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt index 0b5df0d2e0..75f6893c7c 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/TextUtils.kt @@ -19,7 +19,7 @@ package im.vector.riotx.core.utils import android.content.Context import android.os.Build import android.text.format.Formatter -import java.util.* +import java.util.TreeMap object TextUtils { diff --git a/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt new file mode 100644 index 0000000000..3de555f66e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.utils + +import java.net.URL + +fun String.isValidUrl(): Boolean { + return try { + URL(this) + true + } catch (t: Throwable) { + false + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/command/Command.kt b/vector/src/main/java/im/vector/riotx/features/command/Command.kt index 2d4cecdf26..7d745b925b 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/Command.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/Command.kt @@ -37,5 +37,6 @@ enum class Command(val command: String, val parameters: String, @StringRes val d KICK_USER("/kick", " [reason]", R.string.command_description_kick_user), CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick), MARKDOWN("/markdown", "", R.string.command_description_markdown), - CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token); + CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), + SPOILER("/spoiler", "", R.string.command_description_spoiler); } diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt index ac79ed8b40..3f5808949b 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt @@ -44,12 +44,11 @@ object CommandParser { return ParsedCommand.ErrorNotACommand } - var messageParts: List? = null - - try { - messageParts = textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() } + val messageParts = try { + textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() } } catch (e: Exception) { Timber.e(e, "## manageSplashCommand() : split failed") + null } // test if the string cut fails @@ -57,52 +56,50 @@ object CommandParser { return ParsedCommand.ErrorEmptySlashCommand } - val slashCommand = messageParts[0] - - when (slashCommand) { - Command.CHANGE_DISPLAY_NAME.command -> { + return when (val slashCommand = messageParts.first()) { + Command.CHANGE_DISPLAY_NAME.command -> { val newDisplayName = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim() - return if (newDisplayName.isNotEmpty()) { + if (newDisplayName.isNotEmpty()) { ParsedCommand.ChangeDisplayName(newDisplayName) } else { ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME) } } - Command.TOPIC.command -> { + Command.TOPIC.command -> { val newTopic = textMessage.substring(Command.TOPIC.command.length).trim() - return if (newTopic.isNotEmpty()) { + if (newTopic.isNotEmpty()) { ParsedCommand.ChangeTopic(newTopic) } else { ParsedCommand.ErrorSyntax(Command.TOPIC) } } - Command.EMOTE.command -> { + Command.EMOTE.command -> { val message = textMessage.substring(Command.EMOTE.command.length).trim() - return ParsedCommand.SendEmote(message) + ParsedCommand.SendEmote(message) } - Command.JOIN_ROOM.command -> { + Command.JOIN_ROOM.command -> { val roomAlias = textMessage.substring(Command.JOIN_ROOM.command.length).trim() - return if (roomAlias.isNotEmpty()) { + if (roomAlias.isNotEmpty()) { ParsedCommand.JoinRoom(roomAlias) } else { ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) } } - Command.PART.command -> { + Command.PART.command -> { val roomAlias = textMessage.substring(Command.PART.command.length).trim() - return if (roomAlias.isNotEmpty()) { + if (roomAlias.isNotEmpty()) { ParsedCommand.PartRoom(roomAlias) } else { ParsedCommand.ErrorSyntax(Command.PART) } } - Command.INVITE.command -> { - return if (messageParts.size == 2) { + Command.INVITE.command -> { + if (messageParts.size == 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { @@ -114,8 +111,8 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.INVITE) } } - Command.KICK_USER.command -> { - return if (messageParts.size >= 2) { + Command.KICK_USER.command -> { + if (messageParts.size >= 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { val reason = textMessage.substring(Command.KICK_USER.command.length @@ -130,8 +127,8 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.KICK_USER) } } - Command.BAN_USER.command -> { - return if (messageParts.size >= 2) { + Command.BAN_USER.command -> { + if (messageParts.size >= 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { val reason = textMessage.substring(Command.BAN_USER.command.length @@ -146,8 +143,8 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.BAN_USER) } } - Command.UNBAN_USER.command -> { - return if (messageParts.size == 2) { + Command.UNBAN_USER.command -> { + if (messageParts.size == 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { @@ -159,8 +156,8 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.UNBAN_USER) } } - Command.SET_USER_POWER_LEVEL.command -> { - return if (messageParts.size == 3) { + Command.SET_USER_POWER_LEVEL.command -> { + if (messageParts.size == 3) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { val powerLevelsAsString = messageParts[2] @@ -180,7 +177,7 @@ object CommandParser { } } Command.RESET_USER_POWER_LEVEL.command -> { - return if (messageParts.size == 2) { + if (messageParts.size == 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { @@ -192,27 +189,32 @@ object CommandParser { ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL) } } - Command.MARKDOWN.command -> { - return if (messageParts.size == 2) { + Command.MARKDOWN.command -> { + if (messageParts.size == 2) { when { - "on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true) + "on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true) "off".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(false) - else -> ParsedCommand.ErrorSyntax(Command.MARKDOWN) + else -> ParsedCommand.ErrorSyntax(Command.MARKDOWN) } } else { ParsedCommand.ErrorSyntax(Command.MARKDOWN) } } - Command.CLEAR_SCALAR_TOKEN.command -> { - return if (messageParts.size == 1) { + Command.CLEAR_SCALAR_TOKEN.command -> { + if (messageParts.size == 1) { ParsedCommand.ClearScalarToken } else { ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN) } } - else -> { + Command.SPOILER.command -> { + val message = textMessage.substring(Command.SPOILER.command.length).trim() + + ParsedCommand.SendSpoiler(message) + } + else -> { // Unknown command - return ParsedCommand.ErrorUnknownSlashCommand(slashCommand) + ParsedCommand.ErrorUnknownSlashCommand(slashCommand) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt index f6bbed2889..02f5abe540 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt @@ -45,4 +45,5 @@ sealed class ParsedCommand { class ChangeDisplayName(val displayName: String) : ParsedCommand() class SetMarkdown(val enable: Boolean) : ParsedCommand() object ClearScalarToken : ParsedCommand() + class SendSpoiler(val message: String) : ParsedCommand() } diff --git a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt index ec8f1c7fa2..adf8421842 100644 --- a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt +++ b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt @@ -24,7 +24,7 @@ import im.vector.riotx.features.settings.FontScale import im.vector.riotx.features.settings.VectorLocale import im.vector.riotx.features.themes.ThemeUtils import timber.log.Timber -import java.util.* +import java.util.Locale import javax.inject.Inject /** diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt index 54e3a34744..9642c2d8c6 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt @@ -18,10 +18,10 @@ package im.vector.riotx.features.crypto.keys 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.session.Session import im.vector.matrix.android.internal.extensions.foldToCallback +import im.vector.matrix.android.internal.util.awaitCallback import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.writeToFile import kotlinx.coroutines.Dispatchers @@ -36,28 +36,20 @@ class KeysExporter(private val session: Session) { * Export keys and return the file path with the callback */ fun export(context: Context, password: String, callback: MatrixCallback) { - session.exportRoomKeys(password, object : MatrixCallback { - override fun onSuccess(data: ByteArray) { - GlobalScope.launch(Dispatchers.Main) { - withContext(Dispatchers.IO) { - Try { - val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt") + GlobalScope.launch(Dispatchers.Main) { + runCatching { + val data = awaitCallback { session.exportRoomKeys(password, it) } + withContext(Dispatchers.IO) { + val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt") - writeToFile(data, file) + writeToFile(data, file) - addEntryToDownloadManager(context, file, "text/plain") + addEntryToDownloadManager(context, file, "text/plain") - file.absolutePath - } - } - .foldToCallback(callback) + file.absolutePath } - } - - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - }) + }.foldToCallback(callback) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt index 74b2a86bc1..b60e25af04 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysImporter.kt @@ -18,10 +18,11 @@ package im.vector.riotx.features.crypto.keys import android.content.Context import android.net.Uri -import arrow.core.Try import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.extensions.foldToCallback +import im.vector.matrix.android.internal.util.awaitCallback import im.vector.riotx.core.intent.getMimeTypeFromUri import im.vector.riotx.core.resources.openResource import kotlinx.coroutines.Dispatchers @@ -41,8 +42,8 @@ class KeysImporter(private val session: Session) { password: String, callback: MatrixCallback) { GlobalScope.launch(Dispatchers.Main) { - withContext(Dispatchers.IO) { - Try { + runCatching { + withContext(Dispatchers.IO) { val resource = openResource(context, uri, mimetype ?: getMimeTypeFromUri(context, uri)) if (resource?.mContentStream == null) { @@ -51,33 +52,17 @@ class KeysImporter(private val session: Session) { val data: ByteArray try { - data = ByteArray(resource.mContentStream!!.available()) - resource.mContentStream!!.read(data) - resource.mContentStream!!.close() - - data + data = resource.mContentStream!!.use { it.readBytes() } } catch (e: Exception) { - try { - resource.mContentStream!!.close() - } catch (e2: Exception) { - Timber.e(e2, "## importKeys()") - } - + Timber.e(e, "## importKeys()") throw e } + + awaitCallback { + session.importRoomKeys(data, password, null, it) + } } - } - .fold( - { - callback.onFailure(it) - }, - { byteArray -> - session.importRoomKeys(byteArray, - password, - null, - callback) - } - ) + }.foldToCallback(callback) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt index d74a9e9f4f..a12a43d06f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt @@ -21,10 +21,10 @@ import android.content.Intent import androidx.appcompat.app.AlertDialog import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders -import im.vector.fragments.keysbackup.restore.KeysBackupRestoreFromPassphraseFragment import im.vector.riotx.R +import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.SimpleFragmentActivity class KeysBackupRestoreActivity : SimpleFragmentActivity() { @@ -42,20 +42,16 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { override fun initUiAndData() { super.initUiAndData() - viewModel = ViewModelProviders.of(this, viewModelFactory).get(KeysBackupRestoreSharedViewModel::class.java) + viewModel = viewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java) viewModel.initSession(session) viewModel.keyVersionResult.observe(this, Observer { keyVersion -> if (keyVersion != null && supportFragmentManager.fragments.isEmpty()) { val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null if (isBackupCreatedFromPassphrase) { - supportFragmentManager.beginTransaction() - .replace(R.id.container, KeysBackupRestoreFromPassphraseFragment.newInstance()) - .commitNow() + replaceFragment(R.id.container, KeysBackupRestoreFromPassphraseFragment::class.java) } else { - supportFragmentManager.beginTransaction() - .replace(R.id.container, KeysBackupRestoreFromKeyFragment.newInstance()) - .commitNow() + replaceFragment(R.id.container, KeysBackupRestoreFromKeyFragment::class.java) } } }) @@ -80,16 +76,11 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { viewModel.navigateEvent.observeEvent(this) { uxStateEvent -> when (uxStateEvent) { KeysBackupRestoreSharedViewModel.NAVIGATE_TO_RECOVER_WITH_KEY -> { - supportFragmentManager.beginTransaction() - .replace(R.id.container, KeysBackupRestoreFromKeyFragment.newInstance()) - .addToBackStack(null) - .commit() + addFragmentToBackstack(R.id.container, KeysBackupRestoreFromKeyFragment::class.java) } KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> { supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - supportFragmentManager.beginTransaction() - .replace(R.id.container, KeysBackupRestoreSuccessFragment.newInstance()) - .commit() + replaceFragment(R.id.container, KeysBackupRestoreSuccessFragment::class.java) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt index 3c9ebc3a50..730c92a319 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt @@ -22,21 +22,20 @@ import android.text.Editable import android.view.inputmethod.EditorInfo import android.widget.EditText import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import butterknife.BindView import butterknife.OnClick import butterknife.OnTextChanged import com.google.android.material.textfield.TextInputLayout import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.startImportTextFromFileIntent import timber.log.Timber +import javax.inject.Inject -class KeysBackupRestoreFromKeyFragment : VectorBaseFragment() { +class KeysBackupRestoreFromKeyFragment @Inject constructor() + : VectorBaseFragment() { companion object { - fun newInstance() = KeysBackupRestoreFromKeyFragment() private const val REQUEST_TEXT_FILE_GET = 1 } @@ -51,17 +50,10 @@ class KeysBackupRestoreFromKeyFragment : VectorBaseFragment() { @BindView(R.id.keys_restore_key_enter_edittext) lateinit var mKeyTextEdit: EditText - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = ViewModelProviders.of(this, viewModelFactory).get(KeysBackupRestoreFromKeyViewModel::class.java) - sharedViewModel = activity?.run { - ViewModelProviders.of(this, viewModelFactory).get(KeysBackupRestoreSharedViewModel::class.java) - } ?: throw Exception("Invalid Activity") - + viewModel = fragmentViewModelProvider.get(KeysBackupRestoreFromKeyViewModel::class.java) + sharedViewModel = activityViewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java) mKeyTextEdit.setText(viewModel.recoveryCode.value) mKeyTextEdit.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { @@ -72,7 +64,7 @@ class KeysBackupRestoreFromKeyFragment : VectorBaseFragment() { } mKeyInputLayout.error = viewModel.recoveryCodeErrorText.value - viewModel.recoveryCodeErrorText.observe(this, Observer { newValue -> + viewModel.recoveryCodeErrorText.observe(viewLifecycleOwner, Observer { newValue -> mKeyInputLayout.error = newValue }) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt index c70796e09d..a9bdeee2d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.fragments.keysbackup.restore +package im.vector.riotx.features.crypto.keysbackup.restore import android.content.Context import android.os.Bundle @@ -27,19 +27,16 @@ import android.widget.ImageView import android.widget.TextView import androidx.core.text.set import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import butterknife.BindView import butterknife.OnClick import butterknife.OnTextChanged import com.google.android.material.textfield.TextInputLayout import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel -import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel +import javax.inject.Inject -class KeysBackupRestoreFromPassphraseFragment : VectorBaseFragment() { +class KeysBackupRestoreFromPassphraseFragment @Inject constructor(): VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_from_passphrase @@ -63,29 +60,19 @@ class KeysBackupRestoreFromPassphraseFragment : VectorBaseFragment() { viewModel.showPasswordMode.value = !(viewModel.showPasswordMode.value ?: false) } - companion object { - fun newInstance() = KeysBackupRestoreFromPassphraseFragment() - } - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = ViewModelProviders.of(this, viewModelFactory).get(KeysBackupRestoreFromPassphraseViewModel::class.java) - sharedViewModel = activity?.run { - ViewModelProviders.of(this, viewModelFactory).get(KeysBackupRestoreSharedViewModel::class.java) - } ?: throw Exception("Invalid Activity") + viewModel = fragmentViewModelProvider.get(KeysBackupRestoreFromPassphraseViewModel::class.java) + sharedViewModel = activityViewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java) - viewModel.passphraseErrorText.observe(this, Observer { newValue -> + viewModel.passphraseErrorText.observe(viewLifecycleOwner, Observer { newValue -> mPassphraseInputLayout.error = newValue }) helperTextWithLink.text = spannableStringForHelperText(context!!) - viewModel.showPasswordMode.observe(this, Observer { + viewModel.showPasswordMode.observe(viewLifecycleOwner, Observer { val shouldBeVisible = it ?: false mPassphraseTextEdit.showPassword(shouldBeVisible) mPassphraseReveal.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt index cb55d664ec..0f681af737 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt @@ -17,15 +17,14 @@ package im.vector.riotx.features.crypto.keysbackup.restore import android.os.Bundle import android.widget.TextView -import androidx.lifecycle.ViewModelProviders import butterknife.BindView import butterknife.OnClick import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.LiveEvent +import javax.inject.Inject -class KeysBackupRestoreSuccessFragment : VectorBaseFragment() { +class KeysBackupRestoreSuccessFragment @Inject constructor() : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_success @@ -36,15 +35,9 @@ class KeysBackupRestoreSuccessFragment : VectorBaseFragment() { private lateinit var sharedViewModel: KeysBackupRestoreSharedViewModel - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - sharedViewModel = activity?.run { - ViewModelProviders.of(this, viewModelFactory).get(KeysBackupRestoreSharedViewModel::class.java) - } ?: throw Exception("Invalid Activity") + sharedViewModel = activityViewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java) sharedViewModel.importKeyResult?.let { val part1 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part1, @@ -62,8 +55,4 @@ class KeysBackupRestoreSuccessFragment : VectorBaseFragment() { fun onDone() { sharedViewModel.importRoomKeysFinishWithResult.value = LiveEvent(sharedViewModel.importKeyResult!!) } - - companion object { - fun newInstance() = KeysBackupRestoreSuccessFragment() - } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeyBackupSettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeyBackupSettingsAction.kt new file mode 100644 index 0000000000..723be80dfd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeyBackupSettingsAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.crypto.keysbackup.settings + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class KeyBackupSettingsAction : VectorViewModelAction { + object Init : KeyBackupSettingsAction() + object GetKeyBackupTrust : KeyBackupSettingsAction() + object DeleteKeyBackup : KeyBackupSettingsAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt index 2dabaa792a..98e954c5b5 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt @@ -23,6 +23,7 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.viewModel import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.WaitingViewData import javax.inject.Inject @@ -49,11 +50,8 @@ class KeysBackupManageActivity : SimpleFragmentActivity() { override fun initUiAndData() { super.initUiAndData() if (supportFragmentManager.fragments.isEmpty()) { - supportFragmentManager.beginTransaction() - .replace(R.id.container, KeysBackupSettingsFragment.newInstance()) - .commitNow() - - viewModel.init() + replaceFragment(R.id.container, KeysBackupSettingsFragment::class.java) + viewModel.handle(KeyBackupSettingsAction.Init) } // Observe the deletion of keys backup diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt index 2194ca9871..9994ee5002 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt @@ -21,29 +21,20 @@ import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity import kotlinx.android.synthetic.main.fragment_keys_backup_settings.* import javax.inject.Inject -class KeysBackupSettingsFragment : VectorBaseFragment(), - KeysBackupSettingsRecyclerViewController.Listener { - - companion object { - fun newInstance() = KeysBackupSettingsFragment() - } +class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSettingsRecyclerViewController: KeysBackupSettingsRecyclerViewController) + : VectorBaseFragment(), + KeysBackupSettingsRecyclerViewController.Listener { override fun getLayoutResId() = R.layout.fragment_keys_backup_settings - @Inject lateinit var keysBackupSettingsRecyclerViewController: KeysBackupSettingsRecyclerViewController private val viewModel: KeysBackupSettingsViewModel by activityViewModel() - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -75,7 +66,7 @@ class KeysBackupSettingsFragment : VectorBaseFragment(), .setMessage(R.string.keys_backup_settings_delete_confirm_message) .setCancelable(false) .setPositiveButton(R.string.keys_backup_settings_delete_confirm_title) { _, _ -> - viewModel.deleteCurrentBackup() + viewModel.handle(KeyBackupSettingsAction.DeleteKeyBackup) } .setNegativeButton(R.string.cancel, null) .setCancelable(true) @@ -84,10 +75,10 @@ class KeysBackupSettingsFragment : VectorBaseFragment(), } override fun loadTrustData() { - viewModel.getKeysBackupTrust() + viewModel.handle(KeyBackupSettingsAction.GetKeyBackupTrust) } override fun loadKeysBackupState() { - viewModel.init() + viewModel.handle(KeyBackupSettingsAction.Init) } } 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 6b01a7dffa..7b60cb2f9b 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 @@ -31,7 +31,7 @@ import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.ui.list.GenericItem import im.vector.riotx.core.ui.list.genericItem -import java.util.* +import java.util.UUID import javax.inject.Inject class KeysBackupSettingsRecyclerViewController @Inject constructor(private val stringProvider: StringProvider, diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt index 67690b838a..4acb318033 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt @@ -15,13 +15,7 @@ */ package im.vector.riotx.features.crypto.keysbackup.settings -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback @@ -34,8 +28,8 @@ import im.vector.riotx.core.platform.VectorViewModel class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialState: KeysBackupSettingViewState, session: Session -) : VectorViewModel(initialState), - KeysBackupStateListener { +) : VectorViewModel(initialState), + KeysBackupStateListener { @AssistedInject.Factory interface Factory { @@ -64,11 +58,19 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS getKeysBackupTrust() } - fun init() { + override fun handle(action: KeyBackupSettingsAction) { + when (action) { + KeyBackupSettingsAction.Init -> init() + KeyBackupSettingsAction.GetKeyBackupTrust -> getKeysBackupTrust() + KeyBackupSettingsAction.DeleteKeyBackup -> deleteCurrentBackup() + } + } + + private fun init() { keysBackupService.forceUsingLastVersion(object : MatrixCallback {}) } - fun getKeysBackupTrust() = withState { state -> + private fun getKeysBackupTrust() = withState { state -> val versionResult = keysBackupService.keysBackupVersion if (state.keysBackupVersionTrust is Uninitialized && versionResult != null) { @@ -116,7 +118,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS getKeysBackupTrust() } - fun deleteCurrentBackup() { + private fun deleteCurrentBackup() { val keysBackupService = keysBackupService if (keysBackupService.currentBackupVersion != null) { @@ -153,6 +155,6 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS val currentBackupState = keysBackupService.state return currentBackupState == KeysBackupState.Unknown - || currentBackupState == KeysBackupState.CheckingBackUpOnHomeserver + || currentBackupState == KeysBackupState.CheckingBackUpOnHomeserver } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index 6868fb84bb..b77620b15b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -21,11 +21,11 @@ import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import im.vector.matrix.android.api.MatrixCallback import im.vector.riotx.R import im.vector.riotx.core.dialogs.ExportKeysDialog import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.utils.* import im.vector.riotx.features.crypto.keys.KeysExporter @@ -39,12 +39,10 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { override fun initUiAndData() { super.initUiAndData() if (isFirstCreation()) { - supportFragmentManager.beginTransaction() - .replace(R.id.container, KeysBackupSetupStep1Fragment.newInstance()) - .commitNow() + replaceFragment(R.id.container, KeysBackupSetupStep1Fragment::class.java) } - viewModel = ViewModelProviders.of(this, viewModelFactory).get(KeysBackupSetupSharedViewModel::class.java) + viewModel = viewModelProvider.get(KeysBackupSetupSharedViewModel::class.java) viewModel.showManualExport.value = intent.getBooleanExtra(EXTRA_SHOW_MANUAL_EXPORT, false) viewModel.initSession(session) @@ -67,15 +65,11 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { when (uxStateEvent) { KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_2 -> { supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - supportFragmentManager.beginTransaction() - .replace(R.id.container, KeysBackupSetupStep2Fragment.newInstance()) - .commit() + replaceFragment(R.id.container, KeysBackupSetupStep2Fragment::class.java) } KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_3 -> { supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - supportFragmentManager.beginTransaction() - .replace(R.id.container, KeysBackupSetupStep3Fragment.newInstance()) - .commit() + replaceFragment(R.id.container, KeysBackupSetupStep3Fragment::class.java) } KeysBackupSetupSharedViewModel.NAVIGATE_FINISH -> { val resultIntent = Intent() diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt index 8ece6af714..a9bfbd83b0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt @@ -21,19 +21,14 @@ import android.view.View import android.widget.Button import android.widget.TextView import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import butterknife.BindView import butterknife.OnClick import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.LiveEvent +import javax.inject.Inject -class KeysBackupSetupStep1Fragment : VectorBaseFragment() { - - companion object { - fun newInstance() = KeysBackupSetupStep1Fragment() - } +class KeysBackupSetupStep1Fragment @Inject constructor() : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step1 @@ -45,18 +40,12 @@ class KeysBackupSetupStep1Fragment : VectorBaseFragment() { @BindView(R.id.keys_backup_setup_step1_manualExport) lateinit var manualExportButton: Button - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = activity?.run { - ViewModelProviders.of(this, viewModelFactory).get(KeysBackupSetupSharedViewModel::class.java) - } ?: throw Exception("Invalid Activity") + viewModel = activityViewModelProvider.get(KeysBackupSetupSharedViewModel::class.java) - viewModel.showManualExport.observe(this, Observer { + viewModel.showManualExport.observe(viewLifecycleOwner, Observer { val showOption = it ?: false // Can't use isVisible because the kotlin compiler will crash with Back-end (JVM) Internal error: wrong code generated advancedOptionText.visibility = if (showOption) View.VISIBLE else View.GONE diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt index f4717fa7b3..3522c5a752 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.ImageView import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import androidx.transition.TransitionManager import butterknife.BindView import butterknife.OnClick @@ -30,13 +29,13 @@ import butterknife.OnTextChanged import com.google.android.material.textfield.TextInputLayout import com.nulabinc.zxcvbn.Zxcvbn import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.PasswordStrengthBar import im.vector.riotx.features.settings.VectorLocale +import javax.inject.Inject -class KeysBackupSetupStep2Fragment : VectorBaseFragment() { +class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step2 @@ -76,16 +75,10 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() { private lateinit var viewModel: KeysBackupSetupSharedViewModel - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = activity?.run { - ViewModelProviders.of(this, viewModelFactory).get(KeysBackupSetupSharedViewModel::class.java) - } ?: throw Exception("Invalid Activity") + viewModel = activityViewModelProvider.get(KeysBackupSetupSharedViewModel::class.java) viewModel.shouldPromptOnBack = true bindViewToViewModel() @@ -96,7 +89,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() { * ========================================================================================== */ private fun bindViewToViewModel() { - viewModel.passwordStrength.observe(this, Observer { strength -> + viewModel.passwordStrength.observe(viewLifecycleOwner, Observer { strength -> if (strength == null) { mPassphraseProgressLevel.strength = 0 mPassphraseInputLayout.error = null @@ -120,7 +113,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() { } }) - viewModel.passphrase.observe(this, Observer { newValue -> + viewModel.passphrase.observe(viewLifecycleOwner, Observer { newValue -> if (newValue.isEmpty()) { viewModel.passwordStrength.value = null } else { @@ -135,21 +128,21 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() { mPassphraseTextEdit.setText(viewModel.passphrase.value) - viewModel.passphraseError.observe(this, Observer { + viewModel.passphraseError.observe(viewLifecycleOwner, Observer { TransitionManager.beginDelayedTransition(rootGroup) mPassphraseInputLayout.error = it }) mPassphraseConfirmTextEdit.setText(viewModel.confirmPassphrase.value) - viewModel.showPasswordMode.observe(this, Observer { + viewModel.showPasswordMode.observe(viewLifecycleOwner, Observer { val shouldBeVisible = it ?: false mPassphraseTextEdit.showPassword(shouldBeVisible) mPassphraseConfirmTextEdit.showPassword(shouldBeVisible) mPassphraseReveal.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) }) - viewModel.confirmPassphraseError.observe(this, Observer { + viewModel.confirmPassphraseError.observe(viewLifecycleOwner, Observer { TransitionManager.beginDelayedTransition(rootGroup) mPassphraseConfirmInputLayout.error = it }) @@ -203,8 +196,4 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() { } } } - - companion object { - fun newInstance() = KeysBackupSetupStep2Fragment() - } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index 7b61ca2c0f..52470a47b2 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -24,13 +24,11 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import arrow.core.Try import butterknife.BindView import butterknife.OnClick import com.google.android.material.bottomsheet.BottomSheetDialog import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.writeToFile import im.vector.riotx.core.platform.VectorBaseFragment @@ -40,8 +38,9 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File +import javax.inject.Inject -class KeysBackupSetupStep3Fragment : VectorBaseFragment() { +class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step3 @@ -54,25 +53,15 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() { @BindView(R.id.keys_backup_setup_step3_line2_text) lateinit var mRecoveryKeyLabel2TextView: TextView - companion object { - fun newInstance() = KeysBackupSetupStep3Fragment() - } - private lateinit var viewModel: KeysBackupSetupSharedViewModel - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = activity?.run { - ViewModelProviders.of(this, viewModelFactory).get(KeysBackupSetupSharedViewModel::class.java) - } ?: throw Exception("Invalid Activity") + viewModel = activityViewModelProvider.get(KeysBackupSetupSharedViewModel::class.java) viewModel.shouldPromptOnBack = false - viewModel.passphrase.observe(this, Observer { + viewModel.passphrase.observe(viewLifecycleOwner, Observer { if (it.isNullOrBlank()) { // Recovery was generated, so show key and options to save mRecoveryKeyLabel2TextView.text = getString(R.string.keys_backup_setup_step3_text_line2_no_passphrase) @@ -170,8 +159,8 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() { private fun exportRecoveryKeyToFile(data: String) { GlobalScope.launch(Dispatchers.Main) { - withContext(Dispatchers.IO) { - Try { + Try { + withContext(Dispatchers.IO) { val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt") diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationActivity.kt index a7fb4c67f3..cf80bf98fc 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationActivity.kt @@ -21,12 +21,12 @@ import android.content.Intent import android.view.MenuItem import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.riotx.R +import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.WaitingViewData @@ -82,7 +82,7 @@ class SASVerificationActivity : SimpleFragmentActivity() { override fun initUiAndData() { super.initUiAndData() - viewModel = ViewModelProviders.of(this, viewModelFactory).get(SasVerificationViewModel::class.java) + viewModel = viewModelProvider.get(SasVerificationViewModel::class.java) val transactionID: String? = intent.getStringExtra(EXTRA_TRANSACTION_ID) if (isFirstCreation()) { @@ -102,23 +102,23 @@ class SASVerificationActivity : SimpleFragmentActivity() { IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT, IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT -> { supportActionBar?.setTitle(R.string.sas_incoming_request_title) - supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) - .replace(R.id.container, SASVerificationIncomingFragment.newInstance()) - .commitNow() + supportFragmentManager.commitTransaction { + setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + replace(R.id.container, SASVerificationIncomingFragment::class.java, null) + } } IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION, IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { - supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) - .replace(R.id.container, SASVerificationShortCodeFragment.newInstance()) - .commitNow() + supportFragmentManager.commitTransaction { + setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + replace(R.id.container, SASVerificationShortCodeFragment::class.java, null) + } } IncomingSasVerificationTransaction.UxState.VERIFIED -> { - supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) - .replace(R.id.container, SASVerificationVerifiedFragment.newInstance()) - .commitNow() + supportFragmentManager.commitTransaction { + setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + replace(R.id.container, SASVerificationVerifiedFragment::class.java, null) + } } IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME, IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER -> { @@ -133,23 +133,23 @@ class SASVerificationActivity : SimpleFragmentActivity() { OutgoingSasVerificationRequest.UxState.UNKNOWN, OutgoingSasVerificationRequest.UxState.WAIT_FOR_START, OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT -> { - supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) - .replace(R.id.container, SASVerificationStartFragment.newInstance()) - .commitNow() + supportFragmentManager.commitTransaction { + setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + replace(R.id.container, SASVerificationStartFragment::class.java, null) + } } OutgoingSasVerificationRequest.UxState.SHOW_SAS, OutgoingSasVerificationRequest.UxState.WAIT_FOR_VERIFICATION -> { - supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) - .replace(R.id.container, SASVerificationShortCodeFragment.newInstance()) - .commitNow() + supportFragmentManager.commitTransaction { + setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + replace(R.id.container, SASVerificationShortCodeFragment::class.java, null) + } } OutgoingSasVerificationRequest.UxState.VERIFIED -> { - supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) - .replace(R.id.container, SASVerificationVerifiedFragment.newInstance()) - .commitNow() + supportFragmentManager.commitTransaction { + setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + replace(R.id.container, SASVerificationVerifiedFragment::class.java, null) + } } OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME, OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER -> { @@ -172,16 +172,16 @@ class SASVerificationActivity : SimpleFragmentActivity() { finish() } SasVerificationViewModel.NAVIGATE_SAS_DISPLAY -> { - supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.enter_from_right, R.anim.exit_fade_out) - .replace(R.id.container, SASVerificationShortCodeFragment.newInstance()) - .commitNow() + supportFragmentManager.commitTransaction { + setCustomAnimations(R.anim.enter_from_right, R.anim.exit_fade_out) + replace(R.id.container, SASVerificationShortCodeFragment::class.java, null) + } } SasVerificationViewModel.NAVIGATE_SUCCESS -> { - supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.enter_from_right, R.anim.exit_fade_out) - .replace(R.id.container, SASVerificationVerifiedFragment.newInstance()) - .commitNow() + supportFragmentManager.commitTransaction { + setCustomAnimations(R.anim.enter_from_right, R.anim.exit_fade_out) + replace(R.id.container, SASVerificationVerifiedFragment::class.java, null) + } } SasVerificationViewModel.NAVIGATE_CANCELLED -> { val isCancelledByMe = viewModel.transaction?.state == SasVerificationTxState.Cancelled diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt index d8d36e0a1d..88df53d0f3 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt @@ -19,21 +19,17 @@ import android.os.Bundle import android.widget.ImageView import android.widget.TextView import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import butterknife.BindView import butterknife.OnClick import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject -class SASVerificationIncomingFragment : VectorBaseFragment() { - - companion object { - fun newInstance() = SASVerificationIncomingFragment() - } +class SASVerificationIncomingFragment @Inject constructor( + private var avatarRenderer: AvatarRenderer +) : VectorBaseFragment() { @BindView(R.id.sas_incoming_request_user_display_name) lateinit var otherUserDisplayNameTextView: TextView @@ -49,19 +45,12 @@ class SASVerificationIncomingFragment : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_sas_verification_incoming_request - @Inject lateinit var avatarRenderer: AvatarRenderer private lateinit var viewModel: SasVerificationViewModel - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = activity?.run { - ViewModelProviders.of(this, viewModelFactory).get(SasVerificationViewModel::class.java) - } ?: throw Exception("Invalid Activity") + viewModel = activityViewModelProvider.get(SasVerificationViewModel::class.java) otherUserDisplayNameTextView.text = viewModel.otherUser?.displayName ?: viewModel.otherUserId otherUserIdTextView.text = viewModel.otherUserId @@ -74,7 +63,7 @@ class SASVerificationIncomingFragment : VectorBaseFragment() { avatarRenderer.render(null, viewModel.otherUserId ?: "", viewModel.otherUserId, avatarImageView) } - viewModel.transactionState.observe(this, Observer { + viewModel.transactionState.observe(viewLifecycleOwner, Observer { val uxState = (viewModel.transaction as? IncomingSasVerificationTransaction)?.uxState when (uxState) { IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt index 1c5488c0cd..ec9a943449 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt @@ -21,22 +21,18 @@ import android.widget.TextView import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import butterknife.BindView import butterknife.OnClick import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment +import javax.inject.Inject -class SASVerificationShortCodeFragment : VectorBaseFragment() { +class SASVerificationShortCodeFragment @Inject constructor(): VectorBaseFragment() { private lateinit var viewModel: SasVerificationViewModel - companion object { - fun newInstance() = SASVerificationShortCodeFragment() - } - @BindView(R.id.sas_decimal_code) lateinit var decimalTextView: TextView @@ -65,9 +61,7 @@ class SASVerificationShortCodeFragment : VectorBaseFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = activity?.run { - ViewModelProviders.of(this, viewModelFactory).get(SasVerificationViewModel::class.java) - } ?: throw Exception("Invalid Activity") + viewModel = activityViewModelProvider.get(SasVerificationViewModel::class.java) viewModel.transaction?.let { if (it.supportsEmoji()) { @@ -120,7 +114,7 @@ class SASVerificationShortCodeFragment : VectorBaseFragment() { } } - viewModel.transactionState.observe(this, Observer { + viewModel.transactionState.observe(viewLifecycleOwner, Observer { if (viewModel.transaction is IncomingSasVerificationTransaction) { val uxState = (viewModel.transaction as IncomingSasVerificationTransaction).uxState when (uxState) { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt index c101b8f9b4..d9c3b1d155 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt @@ -23,7 +23,6 @@ import android.widget.TextView import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import androidx.transition.TransitionManager import butterknife.BindView import butterknife.OnClick @@ -31,12 +30,9 @@ import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRe import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment +import javax.inject.Inject -class SASVerificationStartFragment : VectorBaseFragment() { - - companion object { - fun newInstance() = SASVerificationStartFragment() - } +class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_sas_verification_start @@ -56,8 +52,8 @@ class SASVerificationStartFragment : VectorBaseFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = ViewModelProviders.of(vectorBaseActivity, viewModelFactory).get(SasVerificationViewModel::class.java) - viewModel.transactionState.observe(this, Observer { + viewModel = activityViewModelProvider.get(SasVerificationViewModel::class.java) + viewModel.transactionState.observe(viewLifecycleOwner, Observer { val uxState = (viewModel.transaction as? OutgoingSasVerificationRequest)?.uxState when (uxState) { OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT -> { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationVerifiedFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationVerifiedFragment.kt index b9d0546f3a..17beb21aff 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationVerifiedFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationVerifiedFragment.kt @@ -16,27 +16,21 @@ package im.vector.riotx.features.crypto.verification import android.os.Bundle -import androidx.lifecycle.ViewModelProviders import butterknife.OnClick import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment +import javax.inject.Inject -class SASVerificationVerifiedFragment : VectorBaseFragment() { +class SASVerificationVerifiedFragment @Inject constructor() : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_sas_verification_verified - companion object { - fun newInstance() = SASVerificationVerifiedFragment() - } - private lateinit var viewModel: SasVerificationViewModel override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = activity?.run { - ViewModelProviders.of(this, viewModelFactory).get(SasVerificationViewModel::class.java) - } ?: throw Exception("Invalid Activity") + viewModel = activityViewModelProvider.get(SasVerificationViewModel::class.java) } @OnClick(R.id.sas_verification_verified_done_button) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index af367164fc..104aa301cb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -26,12 +26,10 @@ import androidx.core.view.GravityCompat import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.hideKeyboard -import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity @@ -48,13 +46,7 @@ import javax.inject.Inject class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { - // Supported navigation actions for this Activity - sealed class Navigation { - object OpenDrawer : Navigation() - object OpenGroup : Navigation() - } - - private lateinit var navigationViewModel: HomeNavigationViewModel + private lateinit var sharedActionViewModel: HomeSharedActionViewModel @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @@ -76,25 +68,25 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager) - navigationViewModel = ViewModelProviders.of(this).get(HomeNavigationViewModel::class.java) + sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) drawerLayout.addDrawerListener(drawerListener) if (isFirstCreation()) { - val homeDrawerFragment = HomeDrawerFragment.newInstance() - val loadingDetail = LoadingFragment.newInstance() - replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer) - replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer) + replaceFragment(R.id.homeDetailFragmentContainer, LoadingFragment::class.java) + replaceFragment(R.id.homeDrawerFragmentContainer, HomeDrawerFragment::class.java) } - navigationViewModel.navigateTo.observeEvent(this) { navigation -> - when (navigation) { - is Navigation.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) - is Navigation.OpenGroup -> { - drawerLayout.closeDrawer(GravityCompat.START) - val homeDetailFragment = HomeDetailFragment.newInstance() - replaceFragment(homeDetailFragment, R.id.homeDetailFragmentContainer) + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) + is HomeActivitySharedAction.OpenGroup -> { + drawerLayout.closeDrawer(GravityCompat.START) + replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java) + } + } } - } - } + .disposeOnDestroy() if (intent.getBooleanExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, false)) { notificationDrawerManager.clearAllEvents() @@ -154,7 +146,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { } // Force remote backup state update to update the banner if needed - ViewModelProviders.of(this).get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded() + viewModelProvider.get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded() } override fun configure(toolbar: Toolbar) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt new file mode 100644 index 0000000000..493a14512d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home + +import im.vector.riotx.core.platform.VectorSharedAction + +/** + * Supported navigation actions for [HomeActivity] + */ +sealed class HomeActivitySharedAction : VectorSharedAction { + object OpenDrawer : HomeActivitySharedAction() + object OpenGroup : HomeActivitySharedAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailAction.kt new file mode 100644 index 0000000000..3309f987fd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class HomeDetailAction : VectorViewModelAction { + data class SwitchDisplayMode(val displayMode: RoomListDisplayMode) : HomeDetailAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index 844fd4f5b2..fe98501e73 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -18,9 +18,9 @@ package im.vector.riotx.features.home import android.os.Bundle import android.view.LayoutInflater +import android.view.View import androidx.core.view.forEachIndexed import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.bottomnavigation.BottomNavigationItemView @@ -29,7 +29,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.KeysBackupBanner @@ -45,29 +45,24 @@ private const val INDEX_CATCHUP = 0 private const val INDEX_PEOPLE = 1 private const val INDEX_ROOMS = 2 -class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { +class HomeDetailFragment @Inject constructor( + private val session: Session, + val homeDetailViewModelFactory: HomeDetailViewModel.Factory, + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment(), KeysBackupBanner.Delegate { private val unreadCounterBadgeViews = arrayListOf() private val viewModel: HomeDetailViewModel by fragmentViewModel() - private lateinit var navigationViewModel: HomeNavigationViewModel - - @Inject lateinit var session: Session - @Inject lateinit var homeDetailViewModelFactory: HomeDetailViewModel.Factory - @Inject lateinit var avatarRenderer: AvatarRenderer + private lateinit var sharedActionViewModel: HomeSharedActionViewModel override fun getLayoutResId(): Int { return R.layout.fragment_home_detail } - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - navigationViewModel = ViewModelProviders.of(requireActivity()).get(HomeNavigationViewModel::class.java) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) setupBottomNavigationView() setupToolbar() @@ -95,11 +90,9 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { private fun setupKeysBackupBanner() { // Keys backup banner // Use the SignOutViewModel, it observe the keys backup state and this is what we need here - val model = ViewModelProviders.of(this, viewModelFactory).get(SignOutViewModel::class.java) + val model = fragmentViewModelProvider.get(SignOutViewModel::class.java) - model.init(session) - - model.keysBackupState.observe(this, Observer { keysBackupState -> + model.keysBackupState.observe(viewLifecycleOwner, Observer { keysBackupState -> when (keysBackupState) { null -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) @@ -133,19 +126,19 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { } groupToolbar.title = "" groupToolbarAvatarImageView.setOnClickListener { - navigationViewModel.goTo(HomeActivity.Navigation.OpenDrawer) + sharedActionViewModel.post(HomeActivitySharedAction.OpenDrawer) } } private fun setupBottomNavigationView() { bottomNavigationView.setOnNavigationItemSelectedListener { val displayMode = when (it.itemId) { - R.id.bottom_action_home -> RoomListFragment.DisplayMode.HOME - R.id.bottom_action_people -> RoomListFragment.DisplayMode.PEOPLE - R.id.bottom_action_rooms -> RoomListFragment.DisplayMode.ROOMS - else -> RoomListFragment.DisplayMode.HOME + R.id.bottom_action_home -> RoomListDisplayMode.HOME + R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE + R.id.bottom_action_rooms -> RoomListDisplayMode.ROOMS + else -> RoomListDisplayMode.HOME } - viewModel.switchDisplayMode(displayMode) + viewModel.handle(HomeDetailAction.SwitchDisplayMode(displayMode)) true } @@ -159,27 +152,33 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { } } - private fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) { + private fun switchDisplayMode(displayMode: RoomListDisplayMode) { groupToolbarTitleView.setText(displayMode.titleRes) updateSelectedFragment(displayMode) // Update the navigation view (for when we restore the tabs) bottomNavigationView.selectedItemId = when (displayMode) { - RoomListFragment.DisplayMode.PEOPLE -> R.id.bottom_action_people - RoomListFragment.DisplayMode.ROOMS -> R.id.bottom_action_rooms + RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people + RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms else -> R.id.bottom_action_home } } - private fun updateSelectedFragment(displayMode: RoomListFragment.DisplayMode) { + private fun updateSelectedFragment(displayMode: RoomListDisplayMode) { val fragmentTag = "FRAGMENT_TAG_${displayMode.name}" - var fragment = childFragmentManager.findFragmentByTag(fragmentTag) - if (fragment == null) { - fragment = RoomListFragment.newInstance(RoomListParams(displayMode)) + val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag) + childFragmentManager.commitTransactionNow { + childFragmentManager.fragments + .filter { it != fragmentToShow } + .forEach { + detach(it) + } + if (fragmentToShow == null) { + val params = RoomListParams(displayMode) + add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag) + } else { + attach(fragmentToShow) + } } - childFragmentManager.beginTransaction() - .replace(R.id.roomListContainer, fragment, fragmentTag) - .addToBackStack(fragmentTag) - .commit() } /* ========================================================================================== @@ -201,11 +200,4 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate { unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms)) syncStateView.render(it.syncState) } - - companion object { - - fun newInstance(): HomeDetailFragment { - return HomeDetailFragment() - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index c4dcd79ea0..ca552dc234 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -26,8 +26,7 @@ import im.vector.matrix.rx.rx import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.home.group.SelectedGroupStore -import im.vector.riotx.features.home.room.list.RoomListFragment +import im.vector.riotx.features.home.group.SelectedGroupDataSource import im.vector.riotx.features.ui.UiStateRepository import io.reactivex.schedulers.Schedulers @@ -38,10 +37,10 @@ import io.reactivex.schedulers.Schedulers class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, private val session: Session, private val uiStateRepository: UiStateRepository, - private val selectedGroupStore: SelectedGroupStore, - private val homeRoomListStore: HomeRoomListObservableStore, + private val selectedGroupStore: SelectedGroupDataSource, + private val homeRoomListStore: HomeRoomListDataSource, private val stringProvider: StringProvider) - : VectorViewModel(initialState) { + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -70,13 +69,19 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho observeRoomSummaries() } - fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) = withState { state -> - if (state.displayMode != displayMode) { + override fun handle(action: HomeDetailAction) { + when (action) { + is HomeDetailAction.SwitchDisplayMode -> handleSwitchDisplayMode(action) + } + } + + private fun handleSwitchDisplayMode(action: HomeDetailAction.SwitchDisplayMode) = withState { state -> + if (state.displayMode != action.displayMode) { setState { - copy(displayMode = displayMode) + copy(displayMode = action.displayMode) } - uiStateRepository.storeDisplayMode(displayMode) + uiStateRepository.storeDisplayMode(action.displayMode) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt index b1c50b83cf..c7c5e4a233 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt @@ -17,14 +17,17 @@ package im.vector.riotx.features.home import arrow.core.Option +import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.sync.SyncState -import im.vector.riotx.features.home.room.list.RoomListFragment data class HomeDetailViewState( val groupSummary: Option = Option.empty(), - val displayMode: RoomListFragment.DisplayMode = RoomListFragment.DisplayMode.HOME, + val asyncRooms: Async> = Uninitialized, + val displayMode: RoomListDisplayMode = RoomListDisplayMode.HOME, val notificationCountCatchup: Int = 0, val notificationHighlightCatchup: Boolean = false, val notificationCountPeople: Int = 0, diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index e5f0c5b2d3..422b59671e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -17,9 +17,9 @@ package im.vector.riotx.features.home import android.os.Bundle +import android.view.View import im.vector.matrix.android.api.session.Session import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.observeK import im.vector.riotx.core.extensions.replaceChildFragment import im.vector.riotx.core.platform.VectorBaseFragment @@ -27,29 +27,17 @@ import im.vector.riotx.features.home.group.GroupListFragment import kotlinx.android.synthetic.main.fragment_home_drawer.* import javax.inject.Inject -class HomeDrawerFragment : VectorBaseFragment() { - - companion object { - - fun newInstance(): HomeDrawerFragment { - return HomeDrawerFragment() - } - } - - @Inject lateinit var session: Session - @Inject lateinit var avatarRenderer: AvatarRenderer +class HomeDrawerFragment @Inject constructor( + private val session: Session, + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_home_drawer - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) if (savedInstanceState == null) { - val groupListFragment = GroupListFragment.newInstance() - replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer) + replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java) } session.liveUser(session.myUserId).observeK(this) { optionalUser -> val user = optionalUser?.getOrNull() diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeRoomListObservableStore.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeRoomListDataSource.kt similarity index 84% rename from vector/src/main/java/im/vector/riotx/features/home/HomeRoomListObservableStore.kt rename to vector/src/main/java/im/vector/riotx/features/home/HomeRoomListDataSource.kt index df8cd411bb..c27a58d177 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeRoomListObservableStore.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeRoomListDataSource.kt @@ -17,9 +17,9 @@ package im.vector.riotx.features.home import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.riotx.core.utils.RxStore +import im.vector.riotx.core.utils.BehaviorDataSource import javax.inject.Inject import javax.inject.Singleton @Singleton -class HomeRoomListObservableStore @Inject constructor() : RxStore>() +class HomeRoomListDataSource @Inject constructor() : BehaviorDataSource>() diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeNavigationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt similarity index 79% rename from vector/src/main/java/im/vector/riotx/features/home/HomeNavigationViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt index e1ab437060..cd81448a0a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeNavigationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt @@ -16,7 +16,7 @@ package im.vector.riotx.features.home -import im.vector.riotx.core.mvrx.NavigationViewModel +import im.vector.riotx.core.platform.VectorSharedActionViewModel import javax.inject.Inject -class HomeNavigationViewModel @Inject constructor() : NavigationViewModel() +class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/riotx/features/home/LoadingFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/LoadingFragment.kt index 379ac69154..e376514955 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/LoadingFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/LoadingFragment.kt @@ -22,15 +22,9 @@ import android.view.View import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_loading.* +import javax.inject.Inject -class LoadingFragment : VectorBaseFragment() { - - companion object { - - fun newInstance(): LoadingFragment { - return LoadingFragment() - } - } +class LoadingFragment @Inject constructor(): VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_loading diff --git a/matrix-sdk-android-rx/src/test/java/im/vector/matrix/rx/ExampleUnitTest.java b/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt similarity index 61% rename from matrix-sdk-android-rx/src/test/java/im/vector/matrix/rx/ExampleUnitTest.java rename to vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt index 6b7fcfe7e6..18b901a967 100644 --- a/matrix-sdk-android-rx/src/test/java/im/vector/matrix/rx/ExampleUnitTest.java +++ b/vector/src/main/java/im/vector/riotx/features/home/RoomListDisplayMode.kt @@ -14,20 +14,15 @@ * limitations under the License. */ -package im.vector.matrix.rx; +package im.vector.riotx.features.home -import org.junit.Test; +import androidx.annotation.StringRes +import im.vector.riotx.R -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); +enum class RoomListDisplayMode(@StringRes val titleRes: Int) { + HOME(R.string.bottom_action_home), + PEOPLE(R.string.bottom_action_people_x), + ROOMS(R.string.bottom_action_rooms), + FILTERED(/* Not used */ 0), + SHARE(/* Not used */ 0) } -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomAction.kt similarity index 81% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt rename to vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomAction.kt index 8f30c9e559..8410a95707 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomAction.kt @@ -17,13 +17,13 @@ package im.vector.riotx.features.home.createdirect import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.core.platform.VectorViewModelAction -sealed class CreateDirectRoomActions { - - object CreateRoomAndInviteSelectedUsers : CreateDirectRoomActions() - data class FilterKnownUsers(val value: String) : CreateDirectRoomActions() - data class SearchDirectoryUsers(val value: String) : CreateDirectRoomActions() - object ClearFilterKnownUsers : CreateDirectRoomActions() - data class SelectUser(val user: User) : CreateDirectRoomActions() - data class RemoveSelectedUser(val user: User) : CreateDirectRoomActions() +sealed class CreateDirectRoomAction : VectorViewModelAction { + object CreateRoomAndInviteSelectedUsers : CreateDirectRoomAction() + data class FilterKnownUsers(val value: String) : CreateDirectRoomAction() + data class SearchDirectoryUsers(val value: String) : CreateDirectRoomAction() + object ClearFilterKnownUsers : CreateDirectRoomAction() + data class SelectUser(val user: User) : CreateDirectRoomAction() + data class RemoveSelectedUser(val user: User) : CreateDirectRoomAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt index a94b2b85da..f7a68d4552 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt @@ -23,7 +23,6 @@ import android.content.Intent import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.* import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.riotx.R @@ -31,7 +30,6 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack -import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.WaitingViewData import kotlinx.android.synthetic.main.activity.* @@ -39,14 +37,8 @@ import javax.inject.Inject class CreateDirectRoomActivity : SimpleFragmentActivity() { - sealed class Navigation { - object UsersDirectory : Navigation() - object Close : Navigation() - object Previous : Navigation() - } - private val viewModel: CreateDirectRoomViewModel by viewModel() - lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel + private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter @@ -58,16 +50,20 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) toolbar.visibility = View.GONE - navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java) - navigationViewModel.navigateTo.observeEvent(this) { navigation -> - when (navigation) { - is Navigation.UsersDirectory -> addFragmentToBackstack(CreateDirectRoomDirectoryUsersFragment(), R.id.container) - Navigation.Close -> finish() - Navigation.Previous -> onBackPressed() - } - } + sharedActionViewModel = viewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + CreateDirectRoomSharedAction.OpenUsersDirectory -> + addFragmentToBackstack(R.id.container, CreateDirectRoomDirectoryUsersFragment::class.java) + CreateDirectRoomSharedAction.Close -> finish() + CreateDirectRoomSharedAction.GoBack -> onBackPressed() + } + } + .disposeOnDestroy() if (isFirstCreation()) { - addFragment(CreateDirectRoomKnownUsersFragment(), R.id.container) + addFragment(R.id.container, CreateDirectRoomKnownUsersFragment::class.java) } viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) { renderCreateAndInviteState(it) diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt index 6125d1b6b9..cf6abf12e9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -18,36 +18,32 @@ package im.vector.riotx.features.home.createdirect import android.content.Context import android.os.Bundle +import android.view.View import android.view.inputmethod.InputMethodManager -import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.setupAsSearch import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* import javax.inject.Inject -class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), DirectoryUsersController.Callback { +class CreateDirectRoomDirectoryUsersFragment @Inject constructor( + private val directRoomController: DirectoryUsersController +) : VectorBaseFragment(), DirectoryUsersController.Callback { override fun getLayoutResId() = R.layout.fragment_create_direct_room_directory_users private val viewModel: CreateDirectRoomViewModel by activityViewModel() - @Inject lateinit var directRoomController: DirectoryUsersController - private lateinit var navigationViewModel: CreateDirectRoomNavigationViewModel + private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - navigationViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) setupRecyclerView() setupSearchByMatrixIdView() setupCloseView() @@ -64,9 +60,9 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), DirectoryUs createDirectRoomSearchById .textChanges() .subscribe { - viewModel.handle(CreateDirectRoomActions.SearchDirectoryUsers(it.toString())) + viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(it.toString())) } - .disposeOnDestroy() + .disposeOnDestroyView() createDirectRoomSearchById.requestFocus() val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager imm?.showSoftInput(createDirectRoomSearchById, InputMethodManager.SHOW_IMPLICIT) @@ -74,7 +70,7 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), DirectoryUs private fun setupCloseView() { createDirectRoomClose.setOnClickListener { - navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.Previous) + sharedActionViewModel.post(CreateDirectRoomSharedAction.GoBack) } } @@ -84,12 +80,12 @@ class CreateDirectRoomDirectoryUsersFragment : VectorBaseFragment(), DirectoryUs override fun onItemClick(user: User) { view?.hideKeyboard() - viewModel.handle(CreateDirectRoomActions.SelectUser(user)) - navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.Previous) + viewModel.handle(CreateDirectRoomAction.SelectUser(user)) + sharedActionViewModel.post(CreateDirectRoomSharedAction.GoBack) } override fun retryDirectoryUsersRequest() { val currentSearch = createDirectRoomSearchById.text.toString() - viewModel.handle(CreateDirectRoomActions.SearchDirectoryUsers(currentSearch)) + viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(currentSearch)) } } 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 04e8d16fd7..12019fa39e 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 @@ -21,9 +21,9 @@ package im.vector.riotx.features.home.createdirect import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.View import android.widget.ScrollView import androidx.core.view.size -import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState import com.google.android.material.chip.Chip @@ -31,36 +31,29 @@ import com.google.android.material.chip.ChipGroup import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.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.DimensionConverter -import im.vector.riotx.features.home.AvatarRenderer import kotlinx.android.synthetic.main.fragment_create_direct_room.* import javax.inject.Inject -class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersController.Callback { +class CreateDirectRoomKnownUsersFragment @Inject constructor( + private val knownUsersController: KnownUsersController, + private val dimensionConverter: DimensionConverter +) : VectorBaseFragment(), KnownUsersController.Callback { override fun getLayoutResId() = R.layout.fragment_create_direct_room override fun getMenuRes() = R.menu.vector_create_direct_room private val viewModel: CreateDirectRoomViewModel by activityViewModel() + private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel - @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) { - injector.inject(this) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - navigationViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(CreateDirectRoomNavigationViewModel::class.java) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) vectorBaseActivity.setSupportActionBar(createDirectRoomToolbar) setupRecyclerView() setupFilterView() @@ -86,7 +79,7 @@ class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersContr override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_create_direct_room -> { - viewModel.handle(CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers) + viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers) true } else -> @@ -96,7 +89,7 @@ class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersContr private fun setupAddByMatrixIdView() { addByMatrixId.setOnClickListener { - navigationViewModel.goTo(CreateDirectRoomActivity.Navigation.UsersDirectory) + sharedActionViewModel.post(CreateDirectRoomSharedAction.OpenUsersDirectory) } } @@ -104,8 +97,8 @@ class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersContr recyclerView.setHasFixedSize(true) // Don't activate animation as we might have way to much item animation when filtering recyclerView.itemAnimator = null - directRoomController.callback = this - recyclerView.setController(directRoomController) + knownUsersController.callback = this + recyclerView.setController(knownUsersController) } private fun setupFilterView() { @@ -115,13 +108,13 @@ class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersContr .subscribe { text -> val filterValue = text.trim() val action = if (filterValue.isBlank()) { - CreateDirectRoomActions.ClearFilterKnownUsers + CreateDirectRoomAction.ClearFilterKnownUsers } else { - CreateDirectRoomActions.FilterKnownUsers(filterValue.toString()) + CreateDirectRoomAction.FilterKnownUsers(filterValue.toString()) } viewModel.handle(action) } - .disposeOnDestroy() + .disposeOnDestroyView() createDirectRoomFilter.setupAsSearch() createDirectRoomFilter.requestFocus() @@ -134,7 +127,7 @@ class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersContr } override fun invalidate() = withState(viewModel) { - directRoomController.setData(it) + knownUsersController.setData(it) } private fun updateChipsView(data: SelectUserAction) { @@ -164,7 +157,7 @@ class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersContr chip.isCloseIconVisible = true chipGroup.addView(chip) chip.setOnCloseIconClickListener { - viewModel.handle(CreateDirectRoomActions.RemoveSelectedUser(user)) + viewModel.handle(CreateDirectRoomAction.RemoveSelectedUser(user)) } chipGroupScrollView.post { chipGroupScrollView.fullScroll(ScrollView.FOCUS_DOWN) @@ -173,6 +166,6 @@ class CreateDirectRoomKnownUsersFragment : VectorBaseFragment(), KnownUsersContr override fun onItemClick(user: User) { view?.hideKeyboard() - viewModel.handle(CreateDirectRoomActions.SelectUser(user)) + viewModel.handle(CreateDirectRoomAction.SelectUser(user)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedAction.kt new file mode 100644 index 0000000000..0df6720734 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.createdirect + +import im.vector.riotx.core.platform.VectorSharedAction + +sealed class CreateDirectRoomSharedAction : VectorSharedAction { + object OpenUsersDirectory : CreateDirectRoomSharedAction() + object Close : CreateDirectRoomSharedAction() + object GoBack : CreateDirectRoomSharedAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedActionViewModel.kt similarity index 77% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedActionViewModel.kt index e57d58a137..590a4855f5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomNavigationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedActionViewModel.kt @@ -16,7 +16,7 @@ package im.vector.riotx.features.home.createdirect -import im.vector.riotx.core.mvrx.NavigationViewModel +import im.vector.riotx.core.platform.VectorSharedActionViewModel import javax.inject.Inject -class CreateDirectRoomNavigationViewModel @Inject constructor(): NavigationViewModel() +class CreateDirectRoomSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt index aef2340478..f4cd81436c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt @@ -51,7 +51,7 @@ data class SelectUserAction( class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateDirectRoomViewState, private val session: Session) - : VectorViewModel(initialState) { + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -79,14 +79,14 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted observeDirectoryUsers() } - fun handle(action: CreateDirectRoomActions) { + override fun handle(action: CreateDirectRoomAction) { when (action) { - is CreateDirectRoomActions.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() - is CreateDirectRoomActions.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value)) - is CreateDirectRoomActions.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) - is CreateDirectRoomActions.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value) - is CreateDirectRoomActions.SelectUser -> handleSelectUser(action) - is CreateDirectRoomActions.RemoveSelectedUser -> handleRemoveSelectedUser(action) + is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() + is CreateDirectRoomAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value)) + is CreateDirectRoomAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) + is CreateDirectRoomAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value) + is CreateDirectRoomAction.SelectUser -> handleSelectUser(action) + is CreateDirectRoomAction.RemoveSelectedUser -> handleRemoveSelectedUser(action) } } @@ -105,14 +105,14 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } } - private fun handleRemoveSelectedUser(action: CreateDirectRoomActions.RemoveSelectedUser) = withState { state -> + private fun handleRemoveSelectedUser(action: CreateDirectRoomAction.RemoveSelectedUser) = withState { state -> val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId } val selectedUsers = state.selectedUsers.minus(action.user) setState { copy(selectedUsers = selectedUsers) } _selectUserEvent.postLiveEvent(SelectUserAction(action.user, false, index)) } - private fun handleSelectUser(action: CreateDirectRoomActions.SelectUser) = withState { state -> + private fun handleSelectUser(action: CreateDirectRoomAction.SelectUser) = withState { state -> // Reset the filter asap directoryUsersSearch.accept("") val isAddOperation: Boolean diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListActions.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListAction.kt similarity index 85% rename from vector/src/main/java/im/vector/riotx/features/home/group/GroupListActions.kt rename to vector/src/main/java/im/vector/riotx/features/home/group/GroupListAction.kt index 7e0a36d032..e81890e7f2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListAction.kt @@ -17,8 +17,8 @@ package im.vector.riotx.features.home.group import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.riotx.core.platform.VectorViewModelAction -sealed class GroupListActions { - - data class SelectGroup(val groupSummary: GroupSummary) : GroupListActions() +sealed class GroupListAction : VectorViewModelAction { + data class SelectGroup(val groupSummary: GroupSummary) : GroupListAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt index 54c2044ae4..39f8c17f05 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt @@ -17,50 +17,39 @@ package im.vector.riotx.features.home.group import android.os.Bundle -import androidx.lifecycle.ViewModelProviders +import android.view.View import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.home.HomeActivity -import im.vector.riotx.features.home.HomeNavigationViewModel +import im.vector.riotx.features.home.HomeSharedActionViewModel +import im.vector.riotx.features.home.HomeActivitySharedAction import kotlinx.android.synthetic.main.fragment_group_list.* import javax.inject.Inject -class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback { +class GroupListFragment @Inject constructor( + val groupListViewModelFactory: GroupListViewModel.Factory, + private val groupController: GroupSummaryController +) : VectorBaseFragment(), GroupSummaryController.Callback { - companion object { - fun newInstance(): GroupListFragment { - return GroupListFragment() - } - } - - private lateinit var navigationViewModel: HomeNavigationViewModel + private lateinit var sharedActionViewModel: HomeSharedActionViewModel private val viewModel: GroupListViewModel by fragmentViewModel() - @Inject lateinit var groupListViewModelFactory: GroupListViewModel.Factory - @Inject lateinit var groupController: GroupSummaryController - override fun getLayoutResId() = R.layout.fragment_group_list - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - navigationViewModel = ViewModelProviders.of(requireActivity()).get(HomeNavigationViewModel::class.java) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) groupController.callback = this stateView.contentView = groupListEpoxyRecyclerView groupListEpoxyRecyclerView.setController(groupController) viewModel.subscribe { renderState(it) } viewModel.openGroupLiveData.observeEvent(this) { - navigationViewModel.goTo(HomeActivity.Navigation.OpenGroup) + sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) } } @@ -73,6 +62,6 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback } override fun onGroupSelected(groupSummary: GroupSummary) { - viewModel.accept(GroupListActions.SelectGroup(groupSummary)) + viewModel.handle(GroupListAction.SelectGroup(groupSummary)) } } 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 ed547026ee..d9a38d5d9b 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 @@ -39,10 +39,10 @@ import io.reactivex.functions.BiFunction const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID" class GroupListViewModel @AssistedInject constructor(@Assisted initialState: GroupListViewState, - private val selectedGroupStore: SelectedGroupStore, + private val selectedGroupStore: SelectedGroupDataSource, private val session: Session, private val stringProvider: StringProvider -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -81,15 +81,15 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro } } - fun accept(action: GroupListActions) { + override fun handle(action: GroupListAction) { when (action) { - is GroupListActions.SelectGroup -> handleSelectGroup(action) + is GroupListAction.SelectGroup -> handleSelectGroup(action) } } // PRIVATE METHODS ***************************************************************************** - private fun handleSelectGroup(action: GroupListActions.SelectGroup) = withState { state -> + private fun handleSelectGroup(action: GroupListAction.SelectGroup) = withState { state -> if (state.selectedGroup?.groupId != action.groupSummary.groupId) { setState { copy(selectedGroup = action.groupSummary) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/SelectedGroupStore.kt b/vector/src/main/java/im/vector/riotx/features/home/group/SelectedGroupDataSource.kt similarity index 83% rename from vector/src/main/java/im/vector/riotx/features/home/group/SelectedGroupStore.kt rename to vector/src/main/java/im/vector/riotx/features/home/group/SelectedGroupDataSource.kt index a6291ecbb5..c7b36e1e7f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/SelectedGroupStore.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/SelectedGroupDataSource.kt @@ -18,9 +18,9 @@ package im.vector.riotx.features.home.group import arrow.core.Option import im.vector.matrix.android.api.session.group.model.GroupSummary -import im.vector.riotx.core.utils.RxStore +import im.vector.riotx.core.utils.BehaviorDataSource import javax.inject.Inject import javax.inject.Singleton @Singleton -class SelectedGroupStore @Inject constructor() : RxStore>(Option.empty()) +class SelectedGroupDataSource @Inject constructor() : BehaviorDataSource>(Option.empty()) 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/RoomDetailAction.kt similarity index 58% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index a219d25c09..2e59e70d08 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/RoomDetailAction.kt @@ -21,37 +21,44 @@ import im.vector.matrix.android.api.session.events.model.Event 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 +import im.vector.riotx.core.platform.VectorViewModelAction -sealed class RoomDetailActions { +sealed class RoomDetailAction : VectorViewModelAction { + data class SaveDraft(val draft: String) : RoomDetailAction() + data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailAction() + data class SendMedia(val attachments: List) : RoomDetailAction() + data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction() + data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailAction() + data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailAction() + data class SendReaction(val targetEventId: String, val reaction: String) : RoomDetailAction() + data class UndoReaction(val targetEventId: String, val reaction: String, val reason: String? = "") : RoomDetailAction() + data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailAction() + data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailAction() + data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailAction() + data class SetReadMarkerAction(val eventId: String) : RoomDetailAction() + object MarkAllAsRead : RoomDetailAction() + data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailAction() + data class HandleTombstoneEvent(val event: Event) : RoomDetailAction() + object AcceptInvite : RoomDetailAction() + object RejectInvite : RoomDetailAction() - data class SaveDraft(val draft: String) : RoomDetailActions() - data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() - data class SendMedia(val attachments: List) : RoomDetailActions() - data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions() - data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions() - data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() - data class SendReaction(val targetEventId: String, val reaction: String) : RoomDetailActions() - data class UndoReaction(val targetEventId: String, val reaction: String, val reason: String? = "") : RoomDetailActions() - data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() - data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() - data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions() - data class SetReadMarkerAction(val eventId: String) : RoomDetailActions() - object MarkAllAsRead : RoomDetailActions() - data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() - data class HandleTombstoneEvent(val event: Event) : RoomDetailActions() - object AcceptInvite : RoomDetailActions() - object RejectInvite : RoomDetailActions() + data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction() + data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction() + data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction() + data class ExitSpecialMode(val text: String) : RoomDetailAction() - 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) : RoomDetailAction() + data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() - data class ResendMessage(val eventId: String) : RoomDetailActions() - data class RemoveFailedEcho(val eventId: String) : RoomDetailActions() + data class ReportContent( + val eventId: String, + val senderId: String?, + val reason: String, + val spam: Boolean = false, + val inappropriate: Boolean = false) : RoomDetailAction() - data class ReportContent(val eventId: String, val reason: String, val spam: Boolean = false, val inappropriate: Boolean = false) : RoomDetailActions() + data class IgnoreUser(val userId: String?) : RoomDetailAction() - object ClearSendQueue : RoomDetailActions() - object ResendAll : RoomDetailActions() + object ClearSendQueue : RoomDetailAction() + object ResendAll : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt index 91addb5744..eb8118a0c9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt @@ -38,8 +38,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { if (isFirstCreation()) { val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) ?: return - val roomDetailFragment = RoomDetailFragment.newInstance(roomDetailArgs) - replaceFragment(roomDetailFragment, R.id.roomDetailContainer) + replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs) } } 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 7c4437d6f0..6fdbf94590 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 @@ -40,7 +40,6 @@ import androidx.core.content.ContextCompat import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach -import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -68,7 +67,6 @@ 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.user.model.User import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.error.ErrorFormatter @@ -81,8 +79,6 @@ import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.JumpToReadMarkerView import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.utils.* -import im.vector.riotx.core.utils.Debouncer -import im.vector.riotx.core.utils.createUIHandler import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.ContactAttachment @@ -94,15 +90,15 @@ import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.NavigateToRoomInterceptor import im.vector.riotx.features.home.PermalinkHandler import im.vector.riotx.features.home.getColorFromUserId -import im.vector.riotx.features.home.room.detail.composer.TextComposerActions +import im.vector.riotx.features.home.room.detail.composer.TextComposerAction import im.vector.riotx.features.home.room.detail.composer.TextComposerView import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler +import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedAction import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction +import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet @@ -136,7 +132,22 @@ data class RoomDetailArgs( private const val REACTION_SELECT_REQUEST_CODE = 0 -class RoomDetailFragment : +class RoomDetailFragment @Inject constructor( + private val session: Session, + private val avatarRenderer: AvatarRenderer, + private val timelineEventController: TimelineEventController, + private val commandAutocompletePolicy: CommandAutocompletePolicy, + private val autocompleteCommandPresenter: AutocompleteCommandPresenter, + private val autocompleteUserPresenter: AutocompleteUserPresenter, + private val permalinkHandler: PermalinkHandler, + private val notificationDrawerManager: NotificationDrawerManager, + val roomDetailViewModelFactory: RoomDetailViewModel.Factory, + val textComposerViewModelFactory: TextComposerViewModel.Factory, + private val errorFormatter: ErrorFormatter, + private val eventHtmlRenderer: EventHtmlRenderer, + private val vectorPreferences: VectorPreferences, + private val readMarkerHelper: ReadMarkerHelper +) : VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback, @@ -147,12 +158,6 @@ class RoomDetailFragment : companion object { - fun newInstance(args: RoomDetailArgs): RoomDetailFragment { - return RoomDetailFragment().apply { - setArguments(args) - } - } - /**x * Sanitize the display name. * @@ -180,21 +185,6 @@ class RoomDetailFragment : private val debouncer = Debouncer(createUIHandler()) - @Inject lateinit var session: Session - @Inject lateinit var avatarRenderer: AvatarRenderer - @Inject lateinit var timelineEventController: TimelineEventController - @Inject lateinit var commandAutocompletePolicy: CommandAutocompletePolicy - @Inject lateinit var autocompleteCommandPresenter: AutocompleteCommandPresenter - @Inject lateinit var autocompleteUserPresenter: AutocompleteUserPresenter - @Inject lateinit var permalinkHandler: PermalinkHandler - @Inject lateinit var notificationDrawerManager: NotificationDrawerManager - @Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory - @Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory - @Inject lateinit var errorFormatter: ErrorFormatter - @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer - @Inject lateinit var vectorPreferences: VectorPreferences - @Inject lateinit var readMarkerHelper: ReadMarkerHelper - private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback @@ -202,7 +192,7 @@ class RoomDetailFragment : override fun getMenuRes() = R.menu.menu_timeline - private lateinit var actionViewModel: ActionsHandler + private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var layoutManager: LinearLayoutManager private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var keyboardStateUtils: KeyboardStateUtils @@ -213,19 +203,14 @@ class RoomDetailFragment : private var lockSendButton = false - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) attachmentsHelper = AttachmentsHelper.create(this, this).register() keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(roomToolbar) setupRecyclerView() setupComposer() - setupAttachmentButton() setupInviteView() setupNotificationView() setupJumpToReadMarkerView() @@ -238,9 +223,12 @@ class RoomDetailFragment : val message = requireContext().getString(pair.first, *pair.second.toTypedArray()) showSnackWithMessage(message, Snackbar.LENGTH_LONG) } - actionViewModel.actionCommandEvent.observeEvent(this) { - handleActions(it) - } + sharedActionViewModel + .observe() + .subscribe { + handleActions(it) + } + .disposeOnDestroyView() roomDetailViewModel.navigateToEvent.observeEvent(this) { val scrollPosition = timelineEventController.searchPositionOfEvent(it) @@ -285,16 +273,24 @@ class RoomDetailFragment : roomDetailViewModel.requestLiveData.observeEvent(this) { displayRoomDetailActionResult(it) } + } + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) if (savedInstanceState == null) { when (val sharedData = roomDetailArgs.sharedData) { - is SharedData.Text -> roomDetailViewModel.process(RoomDetailActions.SendMessage(sharedData.text, false)) - is SharedData.Attachments -> roomDetailViewModel.process(RoomDetailActions.SendMedia(sharedData.attachmentData)) + is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false)) + is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData)) null -> Timber.v("No share data to process") } } } + override fun onDestroyView() { + super.onDestroyView() + recyclerView.adapter = null + } + override fun onDestroy() { debouncer.cancelAll() super.onDestroy() @@ -333,7 +329,7 @@ class RoomDetailFragment : private fun setupNotificationView() { notificationAreaView.delegate = object : NotificationAreaView.Delegate { override fun onTombstoneEventClicked(tombstoneEvent: Event) { - roomDetailViewModel.process(RoomDetailActions.HandleTombstoneEvent(tombstoneEvent)) + roomDetailViewModel.handle(RoomDetailAction.HandleTombstoneEvent(tombstoneEvent)) } override fun resendUnsentEvents() { @@ -365,11 +361,11 @@ class RoomDetailFragment : // This a temporary option during dev as it is not super stable // Cancel all pending actions in room queue and post a dummy // Then mark all sending events as undelivered - roomDetailViewModel.process(RoomDetailActions.ClearSendQueue) + roomDetailViewModel.handle(RoomDetailAction.ClearSendQueue) return true } if (item.itemId == R.id.resend_all) { - roomDetailViewModel.process(RoomDetailActions.ResendAll) + roomDetailViewModel.handle(RoomDetailAction.ResendAll) return true } return super.onOptionsItemSelected(item) @@ -409,7 +405,7 @@ class RoomDetailFragment : composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.sendButton.setContentDescription(getString(descriptionRes)) - avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + avatarRenderer.render(event.senderAvatar, event.root.senderId ?: "", event.getDisambiguatedDisplayName(), composerLayout.composerRelatedMessageAvatar) composerLayout.expand { // need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -437,7 +433,7 @@ class RoomDetailFragment : notificationDrawerManager.setCurrentRoom(null) - roomDetailViewModel.process(RoomDetailActions.SaveDraft(composerLayout.composerEditText.text.toString())) + roomDetailViewModel.handle(RoomDetailAction.SaveDraft(composerLayout.composerEditText.text.toString())) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -450,7 +446,7 @@ class RoomDetailFragment : val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) ?: return // TODO check if already reacted with that? - roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction)) + roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction)) } } } @@ -480,7 +476,8 @@ class RoomDetailFragment : jumpToReadMarkerView.render(show, readMarkerId) } } - recyclerView.setController(timelineEventController) + recyclerView.adapter = timelineEventController.adapter + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { @@ -501,6 +498,7 @@ class RoomDetailFragment : } } }) + timelineEventController.callback = this if (vectorPreferences.swipeToReplyIsEnabled()) { @@ -508,7 +506,7 @@ class RoomDetailFragment : override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { (model as? AbsMessageItem)?.attributes?.informationData?.let { val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) + roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) } } @@ -603,42 +601,49 @@ 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 { - roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString())) - } composerLayout.callback = object : TextComposerView.Callback { + override fun onAddAttachment() { + if (!::attachmentTypeSelector.isInitialized) { + attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment) + } + attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing) + } + + override fun onSendMessage(text: String) { + if (lockSendButton) { + Timber.w("Send button is locked") + return + } + if (text.isNotBlank()) { + lockSendButton = true + roomDetailViewModel.handle(RoomDetailAction.SendMessage(text, vectorPreferences.isMarkdownEnabled())) + } + } + + override fun onCloseRelatedMessage() { + roomDetailViewModel.handle(RoomDetailAction.ExitSpecialMode(composerLayout.text.toString())) + } + override fun onRichContentSelected(contentUri: Uri): Boolean { - val shareIntent = Intent().apply { - action = Intent.ACTION_SEND - data = contentUri + // We need WRITE_EXTERNAL permission + return if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this@RoomDetailFragment, PERMISSION_REQUEST_CODE_INCOMING_URI)) { + sendUri(contentUri) + } else { + roomDetailViewModel.pendingUri = contentUri + // Always intercept when we request some permission + true } - val isHandled = attachmentsHelper.handleShareIntent(shareIntent) - if (!isHandled) { - Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show() - } - return isHandled } } } - private fun setupAttachmentButton() { - composerLayout.attachmentButton.setOnClickListener { - if (!::attachmentTypeSelector.isInitialized) { - attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this) - } - attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing) + private fun sendUri(uri: Uri): Boolean { + val shareIntent = Intent(Intent.ACTION_SEND, uri) + val isHandled = attachmentsHelper.handleShareIntent(shareIntent) + if (!isHandled) { + Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show() } + return isHandled } private fun setupInviteView() { @@ -751,7 +756,7 @@ class RoomDetailFragment : .show() } - private fun promptReasonToReportContent(action: SimpleAction.ReportContentCustom) { + private fun promptReasonToReportContent(action: EventSharedAction.ReportContentCustom) { val inflater = requireActivity().layoutInflater val layout = inflater.inflate(R.layout.dialog_report_content, null) @@ -762,13 +767,13 @@ class RoomDetailFragment : .setView(layout) .setPositiveButton(R.string.report_content_custom_submit) { _, _ -> val reason = input.text.toString() - roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, reason)) + roomDetailViewModel.handle(RoomDetailAction.ReportContent(action.eventId, action.senderId, reason)) } .setNegativeButton(R.string.cancel, null) .show() } - private fun displayRoomDetailActionResult(result: Async) { + private fun displayRoomDetailActionResult(result: Async) { when (result) { is Fail -> { AlertDialog.Builder(requireActivity()) @@ -779,14 +784,16 @@ class RoomDetailFragment : } is Success -> { when (val data = result.invoke()) { - is RoomDetailActions.ReportContent -> { + is RoomDetailAction.ReportContent -> { when { data.spam -> { AlertDialog.Builder(requireActivity()) .setTitle(R.string.content_reported_as_spam_title) .setMessage(R.string.content_reported_as_spam_content) .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") } + .setNegativeButton(R.string.block_user) { _, _ -> + roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId)) + } .show() .withColoredButton(DialogInterface.BUTTON_NEGATIVE) } @@ -795,7 +802,9 @@ class RoomDetailFragment : .setTitle(R.string.content_reported_as_inappropriate_title) .setMessage(R.string.content_reported_as_inappropriate_content) .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") } + .setNegativeButton(R.string.block_user) { _, _ -> + roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId)) + } .show() .withColoredButton(DialogInterface.BUTTON_NEGATIVE) } @@ -804,7 +813,9 @@ class RoomDetailFragment : .setTitle(R.string.content_reported_title) .setMessage(R.string.content_reported_content) .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") } + .setNegativeButton(R.string.block_user) { _, _ -> + roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId)) + } .show() .withColoredButton(DialogInterface.BUTTON_NEGATIVE) } @@ -827,7 +838,7 @@ class RoomDetailFragment : showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) } else { // Highlight and scroll to this event - roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, true)) + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true)) } return true } @@ -855,18 +866,18 @@ class RoomDetailFragment : } override fun onEventVisible(event: TimelineEvent) { - roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsVisible(event)) + roomDetailViewModel.handle(RoomDetailAction.TimelineEventTurnsVisible(event)) } override fun onEventInvisible(event: TimelineEvent) { - roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsInvisible(event)) + roomDetailViewModel.handle(RoomDetailAction.TimelineEventTurnsInvisible(event)) } override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) { vectorBaseActivity.notImplemented("encrypted message click") } - override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { + override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { // TODO Use navigator val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) @@ -895,10 +906,10 @@ class RoomDetailFragment : } override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { - val action = RoomDetailActions.DownloadFile(eventId, messageFileContent) + val action = RoomDetailAction.DownloadFile(eventId, messageFileContent) // We need WRITE_EXTERNAL permission if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { - roomDetailViewModel.process(action) + roomDetailViewModel.handle(action) } else { roomDetailViewModel.pendingAction = action } @@ -906,19 +917,34 @@ class RoomDetailFragment : override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (allGranted(grantResults)) { - if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) { - val action = roomDetailViewModel.pendingAction - if (action != null) { - roomDetailViewModel.pendingAction = null - roomDetailViewModel.process(action) + when (requestCode) { + PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> { + val action = roomDetailViewModel.pendingAction + if (action != null) { + roomDetailViewModel.pendingAction = null + roomDetailViewModel.handle(action) + } } - } else if (requestCode == PERMISSION_REQUEST_CODE_PICK_ATTACHMENT) { - val pendingType = attachmentsHelper.pendingType - if (pendingType != null) { - attachmentsHelper.pendingType = null - launchAttachmentProcess(pendingType) + PERMISSION_REQUEST_CODE_INCOMING_URI -> { + val pendingUri = roomDetailViewModel.pendingUri + if (pendingUri != null) { + roomDetailViewModel.pendingUri = null + sendUri(pendingUri) + } + } + PERMISSION_REQUEST_CODE_PICK_ATTACHMENT -> { + val pendingType = attachmentsHelper.pendingType + if (pendingType != null) { + attachmentsHelper.pendingType = null + launchAttachmentProcess(pendingType) + } } } + } else { + // Reset all pending data + roomDetailViewModel.pendingAction = null + roomDetailViewModel.pendingUri = null + attachmentsHelper.pendingType = null } } @@ -927,7 +953,7 @@ class RoomDetailFragment : } override fun onLoadMore(direction: Timeline.Direction) { - roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) + roomDetailViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction)) } override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { @@ -938,6 +964,7 @@ class RoomDetailFragment : val roomId = roomDetailArgs.roomId this.view?.hideKeyboard() + MessageActionsBottomSheet .newInstance(roomId, informationData) .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") @@ -956,10 +983,10 @@ class RoomDetailFragment : override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { if (on) { // we should test the current real state of reaction on this event - roomDetailViewModel.process(RoomDetailActions.SendReaction(informationData.eventId, reaction)) + roomDetailViewModel.handle(RoomDetailAction.SendReaction(informationData.eventId, reaction)) } else { // I need to redact a reaction - roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction)) + roomDetailViewModel.handle(RoomDetailAction.UndoReaction(informationData.eventId, reaction)) } } @@ -1007,35 +1034,35 @@ class RoomDetailFragment : } } if (nextReadMarkerId != null) { - roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId)) + roomDetailViewModel.handle(RoomDetailAction.SetReadMarkerAction(nextReadMarkerId)) } } // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { - textComposerViewModel.process(TextComposerActions.QueryUsers(query)) + textComposerViewModel.handle(TextComposerAction.QueryUsers(query)) } - private fun handleActions(action: SimpleAction) { + private fun handleActions(action: EventSharedAction) { when (action) { - is SimpleAction.AddReaction -> { + is EventSharedAction.AddReaction -> { startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) } - is SimpleAction.ViewReactions -> { + is EventSharedAction.ViewReactions -> { ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } - is SimpleAction.Copy -> { + is EventSharedAction.Copy -> { // I need info about the current selected message :/ copyToClipboard(requireContext(), action.content, false) val msg = requireContext().getString(R.string.copied_to_clipboard) showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) } - is SimpleAction.Delete -> { - roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) + is EventSharedAction.Delete -> { + roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) } - is SimpleAction.Share -> { + is EventSharedAction.Share -> { // TODO current data communication is too limited // Need to now the media type // TODO bad, just POC @@ -1063,10 +1090,10 @@ class RoomDetailFragment : } ) } - is SimpleAction.ViewEditHistory -> { + is EventSharedAction.ViewEditHistory -> { onEditedDecorationClicked(action.messageInformationData) } - is SimpleAction.ViewSource -> { + is EventSharedAction.ViewSource -> { val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) view.findViewById(R.id.event_content_text_view)?.let { it.text = action.content @@ -1077,7 +1104,7 @@ class RoomDetailFragment : .setPositiveButton(R.string.ok, null) .show() } - is SimpleAction.ViewDecryptedSource -> { + is EventSharedAction.ViewDecryptedSource -> { val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) view.findViewById(R.id.event_content_text_view)?.let { it.text = action.content @@ -1088,40 +1115,45 @@ class RoomDetailFragment : .setPositiveButton(R.string.ok, null) .show() } - is SimpleAction.QuickReact -> { + is EventSharedAction.QuickReact -> { // eventId,ClickedOn,Add - roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) + roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } - is SimpleAction.Edit -> { - roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString())) + is EventSharedAction.Edit -> { + roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, composerLayout.text.toString())) } - is SimpleAction.Quote -> { - roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString())) + is EventSharedAction.Quote -> { + roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, composerLayout.text.toString())) } - is SimpleAction.Reply -> { - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString())) + is EventSharedAction.Reply -> { + roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, composerLayout.text.toString())) } - is SimpleAction.CopyPermalink -> { + is EventSharedAction.CopyPermalink -> { val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) } - is SimpleAction.Resend -> { - roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId)) + is EventSharedAction.Resend -> { + roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId)) } - is SimpleAction.Remove -> { - roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId)) + is EventSharedAction.Remove -> { + roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId)) } - is SimpleAction.ReportContentSpam -> { - roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is spam", spam = true)) + is EventSharedAction.ReportContentSpam -> { + roomDetailViewModel.handle(RoomDetailAction.ReportContent( + action.eventId, action.senderId, "This message is spam", spam = true)) } - is SimpleAction.ReportContentInappropriate -> { - roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is inappropriate", inappropriate = true)) + is EventSharedAction.ReportContentInappropriate -> { + roomDetailViewModel.handle(RoomDetailAction.ReportContent( + action.eventId, action.senderId, "This message is inappropriate", inappropriate = true)) } - is SimpleAction.ReportContentCustom -> { + is EventSharedAction.ReportContentCustom -> { promptReasonToReportContent(action) } - else -> { + is EventSharedAction.IgnoreUser -> { + roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId)) + } + else -> { Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } } @@ -1188,22 +1220,22 @@ class RoomDetailFragment : override fun onAcceptInvite() { notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) - roomDetailViewModel.process(RoomDetailActions.AcceptInvite) + roomDetailViewModel.handle(RoomDetailAction.AcceptInvite) } override fun onRejectInvite() { notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) - roomDetailViewModel.process(RoomDetailActions.RejectInvite) + roomDetailViewModel.handle(RoomDetailAction.RejectInvite) } // JumpToReadMarkerView.Callback override fun onJumpToReadMarkerClicked(readMarkerId: String) { - roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false)) + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(readMarkerId, false)) } override fun onClearReadMarkerClicked() { - roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead) + roomDetailViewModel.handle(RoomDetailAction.MarkAllAsRead) } // AttachmentTypeSelectorView.Callback @@ -1230,7 +1262,7 @@ class RoomDetailFragment : // AttachmentsHelper.Callback override fun onContentAttachmentsReady(attachments: List) { - roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments)) + roomDetailViewModel.handle(RoomDetailAction.SendMedia(attachments)) } override fun onAttachmentsProcessFailed() { @@ -1240,6 +1272,6 @@ class RoomDetailFragment : override fun onContactAttachmentReady(contactAttachment: ContactAttachment) { super.onContactAttachmentReady(contactAttachment) val formattedContact = contactAttachment.toHumanReadable() - roomDetailViewModel.process(RoomDetailActions.SendMessage(formattedContact, false)) + roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false)) } } 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 f3934f618c..d2c2c7fdde 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 @@ -16,6 +16,7 @@ package im.vector.riotx.features.home.room.detail +import android.net.Uri import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -48,6 +49,7 @@ import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.subscribeLogError @@ -65,13 +67,14 @@ import java.util.concurrent.TimeUnit class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, userPreferencesProvider: UserPreferencesProvider, private val vectorPreferences: VectorPreferences, + private val stringProvider: StringProvider, private val session: Session -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId)!! private val eventId = initialState.eventId - private val invisibleEventsObservable = BehaviorRelay.create() - private val visibleEventsObservable = BehaviorRelay.create() + private val invisibleEventsObservable = BehaviorRelay.create() + private val visibleEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { TimelineSettings(30, filterEdits = false, @@ -89,12 +92,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private var timeline = room.createTimeline(eventId, timelineSettings) // Can be used for several actions, for a one shot result - private val _requestLiveData = MutableLiveData>>() - val requestLiveData: LiveData>> + private val _requestLiveData = MutableLiveData>>() + val requestLiveData: LiveData>> get() = _requestLiveData // Slot to keep a pending action during permission request - var pendingAction: RoomDetailActions? = null + var pendingAction: RoomDetailAction? = null + // Slot to keep a pending uri during permission request + var pendingUri: Uri? = null @AssistedInject.Factory interface Factory { @@ -124,45 +129,46 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro setState { copy(timeline = this@RoomDetailViewModel.timeline) } } - fun process(action: RoomDetailActions) { + override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailActions.SaveDraft -> handleSaveDraft(action) - is RoomDetailActions.SendMessage -> handleSendMessage(action) - is RoomDetailActions.SendMedia -> handleSendMedia(action) - is RoomDetailActions.TimelineEventTurnsVisible -> handleEventVisible(action) - is RoomDetailActions.TimelineEventTurnsInvisible -> handleEventInvisible(action) - is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action) - is RoomDetailActions.SendReaction -> handleSendReaction(action) - is RoomDetailActions.AcceptInvite -> handleAcceptInvite() - is RoomDetailActions.RejectInvite -> handleRejectInvite() - is RoomDetailActions.RedactAction -> handleRedactEvent(action) - is RoomDetailActions.UndoReaction -> handleUndoReact(action) - is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailActions.ExitSpecialMode -> handleExitSpecialMode(action) - is RoomDetailActions.EnterEditMode -> handleEditAction(action) - is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) - is RoomDetailActions.DownloadFile -> handleDownloadFile(action) - is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action) - is RoomDetailActions.ResendMessage -> handleResendEvent(action) - is RoomDetailActions.RemoveFailedEcho -> handleRemove(action) - is RoomDetailActions.ClearSendQueue -> handleClearSendQueue() - is RoomDetailActions.ResendAll -> handleResendAll() - is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action) - is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead() - is RoomDetailActions.ReportContent -> handleReportContent(action) + is RoomDetailAction.SaveDraft -> handleSaveDraft(action) + is RoomDetailAction.SendMessage -> handleSendMessage(action) + is RoomDetailAction.SendMedia -> handleSendMedia(action) + is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) + is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) + is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) + is RoomDetailAction.SendReaction -> handleSendReaction(action) + is RoomDetailAction.AcceptInvite -> handleAcceptInvite() + is RoomDetailAction.RejectInvite -> handleRejectInvite() + is RoomDetailAction.RedactAction -> handleRedactEvent(action) + is RoomDetailAction.UndoReaction -> handleUndoReact(action) + is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) + is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action) + is RoomDetailAction.EnterEditMode -> handleEditAction(action) + is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) + is RoomDetailAction.DownloadFile -> handleDownloadFile(action) + is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailAction.ResendMessage -> handleResendEvent(action) + is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) + is RoomDetailAction.ClearSendQueue -> handleClearSendQueue() + is RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.SetReadMarkerAction -> handleSetReadMarkerAction(action) + is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailAction.ReportContent -> handleReportContent(action) + is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) } } - private fun handleEventInvisible(action: RoomDetailActions.TimelineEventTurnsInvisible) { + private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) { invisibleEventsObservable.accept(action) } /** * Convert a send mode to a draft and save the draft */ - private fun handleSaveDraft(action: RoomDetailActions.SaveDraft) { + private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) { withState { when (it.sendMode) { is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft)) @@ -205,7 +211,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .disposeOnClear() } - private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { + private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() ?: return @@ -261,7 +267,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro // PRIVATE METHODS ***************************************************************************** - private fun handleSendMessage(action: RoomDetailActions.SendMessage) { + private fun handleSendMessage(action: RoomDetailAction.SendMessage) { withState { state -> when (state.sendMode) { is SendMode.REGULAR -> { @@ -324,6 +330,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) popDraft() } + is ParsedCommand.SendSpoiler -> { + room.sendFormattedTextMessage( + "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})", + "${slashCommandResult.message}" + ) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) + popDraft() + } is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) popDraft() @@ -444,20 +458,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro }) } - private fun handleSendReaction(action: RoomDetailActions.SendReaction) { + private fun handleSendReaction(action: RoomDetailAction.SendReaction) { room.sendReaction(action.targetEventId, action.reaction) } - private fun handleRedactEvent(action: RoomDetailActions.RedactAction) { + private fun handleRedactEvent(action: RoomDetailAction.RedactAction) { val event = room.getTimeLineEvent(action.targetEventId) ?: return room.redactEvent(event.root, action.reason) } - private fun handleUndoReact(action: RoomDetailActions.UndoReaction) { + private fun handleUndoReact(action: RoomDetailAction.UndoReaction) { room.undoReaction(action.targetEventId, action.reaction) } - private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) { + private fun handleUpdateQuickReaction(action: RoomDetailAction.UpdateQuickReactAction) { if (action.add) { room.sendReaction(action.targetEventId, action.selectedReaction) } else { @@ -465,7 +479,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun handleSendMedia(action: RoomDetailActions.SendMedia) { + private fun handleSendMedia(action: RoomDetailAction.SendMedia) { val attachments = action.attachments val homeServerCapabilities = session.getHomeServerCapabilities() val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize @@ -482,19 +496,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun handleEventVisible(action: RoomDetailActions.TimelineEventTurnsVisible) { + private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { if (action.event.root.sendState.isSent()) { // ignore pending/local events visibleEventsObservable.accept(action) } // We need to update this with the related m.replace also (to move read receipt) action.event.annotations?.editSummary?.sourceEvents?.forEach { room.getTimeLineEvent(it)?.let { event -> - visibleEventsObservable.accept(RoomDetailActions.TimelineEventTurnsVisible(event)) + visibleEventsObservable.accept(RoomDetailAction.TimelineEventTurnsVisible(event)) } } } - private fun handleLoadMore(action: RoomDetailActions.LoadMoreTimelineEvents) { + private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) { timeline.paginate(action.direction, PAGINATION_COUNT) } @@ -506,44 +520,47 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro room.join(callback = object : MatrixCallback {}) } - private fun handleEditAction(action: RoomDetailActions.EnterEditMode) { - saveCurrentDraft(action.draft) + private fun handleEditAction(action: RoomDetailAction.EnterEditMode) { + saveCurrentDraft(action.text) room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + setState { copy(sendMode = SendMode.EDIT(timelineEvent, action.text)) } timelineEvent.root.eventId?.let { room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: "")) } } } - private fun handleQuoteAction(action: RoomDetailActions.EnterQuoteMode) { - saveCurrentDraft(action.draft) + private fun handleQuoteAction(action: RoomDetailAction.EnterQuoteMode) { + saveCurrentDraft(action.text) room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) } withState { state -> // Save a new draft and keep the previously entered text, if it was not an edit timelineEvent.root.eventId?.let { if (state.sendMode is SendMode.EDIT) { room.saveDraft(UserDraft.QUOTE(it, "")) } else { - room.saveDraft(UserDraft.QUOTE(it, action.draft)) + room.saveDraft(UserDraft.QUOTE(it, action.text)) } } } } } - private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) { - saveCurrentDraft(action.draft) + private fun handleReplyAction(action: RoomDetailAction.EnterReplyMode) { + saveCurrentDraft(action.text) room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> + setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) } withState { state -> // Save a new draft and keep the previously entered text, if it was not an edit timelineEvent.root.eventId?.let { if (state.sendMode is SendMode.EDIT) { room.saveDraft(UserDraft.REPLY(it, "")) } else { - room.saveDraft(UserDraft.REPLY(it, action.draft)) + room.saveDraft(UserDraft.REPLY(it, action.text)) } } } @@ -564,19 +581,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun handleExitSpecialMode(action: RoomDetailActions.ExitSpecialMode) { + private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) { + setState { copy(sendMode = SendMode.REGULAR(action.text)) } 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)) + room.saveDraft(UserDraft.REGULAR(action.text)) } } } - private fun handleDownloadFile(action: RoomDetailActions.DownloadFile) { + private fun handleDownloadFile(action: RoomDetailAction.DownloadFile) { session.downloadFile( FileService.DownloadMode.TO_EXPORT, action.eventId, @@ -602,7 +620,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro }) } - private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) { + private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { val targetEventId: String = action.eventId val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId val indexOfEvent = timeline.getIndexOfEvent(correctedEventId) @@ -616,7 +634,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro _navigateToEvent.postLiveEvent(correctedEventId) } - private fun handleResendEvent(action: RoomDetailActions.ResendMessage) { + private fun handleResendEvent(action: RoomDetailAction.ResendMessage) { val targetEventId = action.eventId room.getTimeLineEvent(targetEventId)?.let { // State must be UNDELIVERED or Failed @@ -634,7 +652,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - private fun handleRemove(action: RoomDetailActions.RemoveFailedEcho) { + private fun handleRemove(action: RoomDetailAction.RemoveFailedEcho) { val targetEventId = action.eventId room.getTimeLineEvent(targetEventId)?.let { // State must be UNDELIVERED or Failed @@ -669,7 +687,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .disposeOnClear() } - private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { + private fun handleSetReadMarkerAction(action: RoomDetailAction.SetReadMarkerAction) = withState { var readMarkerId = action.eventId val indexOfEvent = timeline.getIndexOfEvent(readMarkerId) // force to set the read marker on the next event @@ -685,7 +703,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro room.markAllAsRead(object : MatrixCallback {}) } - private fun handleReportContent(action: RoomDetailActions.ReportContent) { + private fun handleReportContent(action: RoomDetailAction.ReportContent) { room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback { override fun onSuccess(data: Unit) { _requestLiveData.postValue(LiveEvent(Success(action))) @@ -697,6 +715,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro }) } + private fun handleIgnoreUser(action: RoomDetailAction.IgnoreUser) { + if (action.userId.isNullOrEmpty()) { + return + } + + session.ignoreUserIds(listOf(action.userId), object : MatrixCallback { + override fun onSuccess(data: Unit) { + _requestLiveData.postValue(LiveEvent(Success(action))) + } + + override fun onFailure(failure: Throwable) { + _requestLiveData.postValue(LiveEvent(Fail(failure))) + } + }) + } + private fun observeSyncState() { session.rx() .liveSyncState() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt similarity index 84% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerActions.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt index faf94b130b..5d60fa1cef 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt @@ -16,6 +16,8 @@ package im.vector.riotx.features.home.room.detail.composer -sealed class TextComposerActions { - data class QueryUsers(val query: CharSequence?) : TextComposerActions() +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class TextComposerAction : VectorViewModelAction { + data class QueryUsers(val query: CharSequence?) : TextComposerAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index 0a6d3dde08..32307dc3d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.composer import android.content.Context import android.net.Uri +import android.text.Editable import android.util.AttributeSet import android.view.ViewGroup import android.widget.ImageButton @@ -31,6 +32,7 @@ import androidx.transition.TransitionManager import butterknife.BindView import butterknife.ButterKnife import im.vector.riotx.R +import kotlinx.android.synthetic.main.merge_composer_layout.view.* /** * Encapsulate the timeline composer UX. @@ -39,7 +41,11 @@ import im.vector.riotx.R class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) { - interface Callback : ComposerEditText.Callback + interface Callback : ComposerEditText.Callback { + fun onCloseRelatedMessage() + fun onSendMessage(text: String) + fun onAddAttachment() + } var callback: Callback? = null @@ -62,15 +68,31 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib private val animationDuration = 100L + val text: Editable? + get() = composerEditText.text + init { inflate(context, R.layout.merge_composer_layout, this) ButterKnife.bind(this) collapse(false) - composerEditText.callback = object : Callback, ComposerEditText.Callback { + composerEditText.callback = object : ComposerEditText.Callback { override fun onRichContentSelected(contentUri: Uri): Boolean { return callback?.onRichContentSelected(contentUri) ?: false } } + composerRelatedMessageCloseButton.setOnClickListener { + collapse() + callback?.onCloseRelatedMessage() + } + + sendButton.setOnClickListener { + val textMessage = text?.toString() ?: "" + callback?.onSendMessage(textMessage) + } + + attachmentButton.setOnClickListener { + callback?.onAddAttachment() + } } fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt index 84917d682b..88548e12b4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt @@ -36,7 +36,7 @@ typealias AutocompleteUserQuery = CharSequence class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: TextComposerViewState, private val session: Session -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId)!! private val roomId = initialState.roomId @@ -52,7 +52,7 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: @JvmStatic override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel? { - val fragment : RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() + val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.textComposerViewModelFactory.create(state) } } @@ -61,13 +61,13 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: observeUsersQuery() } - fun process(action: TextComposerActions) { + override fun handle(action: TextComposerAction) { when (action) { - is TextComposerActions.QueryUsers -> handleQueryUsers(action) + is TextComposerAction.QueryUsers -> handleQueryUsers(action) } } - private fun handleQueryUsers(action: TextComposerActions.QueryUsers) { + private fun handleQueryUsers(action: TextComposerAction.QueryUsers) { val query = Option.fromNullable(action.query) usersQueryObservable.accept(query) } @@ -76,19 +76,15 @@ class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: Observable.combineLatest, Option, List>( room.rx().liveRoomMemberIds(), usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS), - BiFunction { roomMembers, query -> - val users = roomMembers - .mapNotNull { - session.getUser(it) - } + BiFunction { roomMemberIds, query -> + val users = roomMemberIds.mapNotNull { session.getUser(it) } val filter = query.orNull() if (filter.isNullOrBlank()) { users } else { users.filter { - it.displayName?.startsWith(prefix = filter, ignoreCase = true) - ?: false + it.displayName?.startsWith(prefix = filter, ignoreCase = true) ?: false } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 8b1296482f..be2f1dd7e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -62,7 +62,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onEventVisible(event: TimelineEvent) fun onRoomCreateLinkClicked(url: String) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) - fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) + fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ActionsHandler.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ActionsHandler.kt deleted file mode 100644 index c9284b6ece..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ActionsHandler.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package im.vector.riotx.features.home.room.detail.timeline.action - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import im.vector.riotx.core.extensions.postLiveEvent -import im.vector.riotx.core.utils.LiveEvent -import javax.inject.Inject - -/** - * Activity shared view model to handle message actions - */ -class ActionsHandler @Inject constructor() : ViewModel() { - - val actionCommandEvent = MutableLiveData>() - - fun fireAction(action: SimpleAction) { - actionCommandEvent.postLiveEvent(action) - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt new file mode 100644 index 0000000000..37d96ad62c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.timeline.action + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorSharedAction +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData + +sealed class EventSharedAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) : VectorSharedAction { + object Separator : + EventSharedAction(0, 0) + + data class AddReaction(val eventId: String) : + EventSharedAction(R.string.message_add_reaction, R.drawable.ic_add_reaction) + + data class Copy(val content: String) : + EventSharedAction(R.string.copy, R.drawable.ic_copy) + + data class Edit(val eventId: String) : + EventSharedAction(R.string.edit, R.drawable.ic_edit) + + data class Quote(val eventId: String) : + EventSharedAction(R.string.quote, R.drawable.ic_quote) + + data class Reply(val eventId: String) : + EventSharedAction(R.string.reply, R.drawable.ic_reply) + + data class Share(val imageUrl: String) : + EventSharedAction(R.string.share, R.drawable.ic_share) + + data class Resend(val eventId: String) : + EventSharedAction(R.string.global_retry, R.drawable.ic_refresh_cw) + + data class Remove(val eventId: String) : + EventSharedAction(R.string.remove, R.drawable.ic_trash) + + data class Delete(val eventId: String) : + EventSharedAction(R.string.delete, R.drawable.ic_delete) + + data class Cancel(val eventId: String) : + EventSharedAction(R.string.cancel, R.drawable.ic_close_round) + + data class ViewSource(val content: String) : + EventSharedAction(R.string.view_source, R.drawable.ic_view_source) + + data class ViewDecryptedSource(val content: String) : + EventSharedAction(R.string.view_decrypted_source, R.drawable.ic_view_source) + + data class CopyPermalink(val eventId: String) : + EventSharedAction(R.string.permalink, R.drawable.ic_permalink) + + data class ReportContent(val eventId: String, val senderId: String?) : + EventSharedAction(R.string.report_content, R.drawable.ic_flag) + + data class ReportContentSpam(val eventId: String, val senderId: String?) : + EventSharedAction(R.string.report_content_spam, R.drawable.ic_report_spam) + + data class ReportContentInappropriate(val eventId: String, val senderId: String?) : + EventSharedAction(R.string.report_content_inappropriate, R.drawable.ic_report_inappropriate) + + data class ReportContentCustom(val eventId: String, val senderId: String?) : + EventSharedAction(R.string.report_content_custom, R.drawable.ic_report_custom) + + data class IgnoreUser(val senderId: String?) : + EventSharedAction(R.string.message_ignore_user, R.drawable.ic_alert_triangle) + + data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : + EventSharedAction(0, 0) + + data class ViewReactions(val messageInformationData: MessageInformationData) : + EventSharedAction(R.string.message_view_reaction, R.drawable.ic_view_reactions) + + data class ViewEditHistory(val messageInformationData: MessageInformationData) : + EventSharedAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history) +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsAction.kt new file mode 100644 index 0000000000..fe8a10d364 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.timeline.action + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class MessageActionsAction : VectorViewModelAction { + object ToggleReportMenu : MessageActionsAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 8aaa7643c2..3f4171f733 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -19,7 +19,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import butterknife.BindView @@ -47,7 +46,7 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message override val showExpanded = true - private lateinit var actionHandlerModel: ActionsHandler + private lateinit var sharedActionViewModel: MessageSharedActionViewModel override fun injectWith(screenComponent: ScreenComponent) { screenComponent.inject(this) @@ -61,7 +60,7 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) + sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) recyclerView.adapter = messageActionsEpoxyController.adapter // Disable item animation @@ -69,12 +68,12 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message messageActionsEpoxyController.listener = this } - override fun didSelectMenuAction(simpleAction: SimpleAction) { - if (simpleAction is SimpleAction.ReportContent) { + override fun didSelectMenuAction(eventAction: EventSharedAction) { + if (eventAction is EventSharedAction.ReportContent) { // Toggle report menu - viewModel.toggleReportMenu() + viewModel.handle(MessageActionsAction.ToggleReportMenu) } else { - actionHandlerModel.fireAction(simpleAction) + sharedActionViewModel.post(eventAction) dismiss() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index d9119f08b3..b561a6df3c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -20,6 +20,7 @@ import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Success import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.R +import im.vector.riotx.core.epoxy.bottomsheet.* import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject @@ -40,7 +41,8 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid bottomSheetItemMessagePreview { id("preview") avatarRenderer(avatarRenderer) - informationData(state.informationData) + avatarUrl(state.informationData.avatarUrl ?: "") + senderId(state.informationData.senderId) senderName(state.senderName()) body(body) time(state.time()) @@ -77,7 +79,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid selecteds(state.quickStates.invoke().map { it.isSelected }) listener(object : BottomSheetItemQuickReactions.Listener { override fun didSelect(emoji: String, selected: Boolean) { - listener?.didSelectMenuAction(SimpleAction.QuickReact(state.eventId, emoji, selected)) + listener?.didSelectMenuAction(EventSharedAction.QuickReact(state.eventId, emoji, selected)) } }) } @@ -90,28 +92,35 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid // Action state.actions()?.forEachIndexed { index, action -> - bottomSheetItemAction { - id("action_$index") - iconRes(action.iconResId) - textRes(action.titleRes) - showExpand(action is SimpleAction.ReportContent) - expanded(state.expendedReportContentMenu) - listener(View.OnClickListener { listener?.didSelectMenuAction(action) }) - } + if (action is EventSharedAction.Separator) { + bottomSheetItemSeparator { + id("separator_$index") + } + } else { + bottomSheetItemAction { + id("action_$index") + iconRes(action.iconResId) + textRes(action.titleRes) + showExpand(action is EventSharedAction.ReportContent) + expanded(state.expendedReportContentMenu) + listener(View.OnClickListener { listener?.didSelectMenuAction(action) }) + destructive(action is EventSharedAction.IgnoreUser) + } - if (action is SimpleAction.ReportContent && state.expendedReportContentMenu) { - // Special case for report content menu: add the submenu - listOf( - SimpleAction.ReportContentSpam(action.eventId), - SimpleAction.ReportContentInappropriate(action.eventId), - SimpleAction.ReportContentCustom(action.eventId) - ).forEachIndexed { indexReport, actionReport -> - bottomSheetItemAction { - id("actionReport_$indexReport") - subMenuItem(true) - iconRes(actionReport.iconResId) - textRes(actionReport.titleRes) - listener(View.OnClickListener { listener?.didSelectMenuAction(actionReport) }) + if (action is EventSharedAction.ReportContent && state.expendedReportContentMenu) { + // Special case for report content menu: add the submenu + listOf( + EventSharedAction.ReportContentSpam(action.eventId, action.senderId), + EventSharedAction.ReportContentInappropriate(action.eventId, action.senderId), + EventSharedAction.ReportContentCustom(action.eventId, action.senderId) + ).forEachIndexed { indexReport, actionReport -> + bottomSheetItemAction { + id("actionReport_$indexReport") + subMenuItem(true) + iconRes(actionReport.iconResId) + textRes(actionReport.titleRes) + listener(View.OnClickListener { listener?.didSelectMenuAction(actionReport) }) + } } } } @@ -119,6 +128,6 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid } interface MessageActionsEpoxyControllerListener { - fun didSelectMenuAction(simpleAction: SimpleAction) + fun didSelectMenuAction(eventAction: EventSharedAction) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 135496264d..102412948b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -61,7 +61,7 @@ data class MessageActionState( // For quick reactions val quickStates: Async> = Uninitialized, // For actions - val actions: Async> = Uninitialized, + val actions: Async> = Uninitialized, val expendedReportContentMenu: Boolean = false ) : MvRxState { @@ -77,7 +77,7 @@ data class MessageActionState( } /** - * Information related to an event and used to display preview in contextual bottomsheet. + * Information related to an event and used to display preview in contextual bottom sheet. */ class MessageActionsViewModel @AssistedInject constructor(@Assisted initialState: MessageActionState, @@ -85,7 +85,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private val session: Session, private val noticeEventFormatter: NoticeEventFormatter, private val stringProvider: StringProvider -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { private val eventId = initialState.eventId private val informationData = initialState.informationData @@ -112,7 +112,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted observeEventAction() } - fun toggleReportMenu() = withState { + override fun handle(action: MessageActionsAction) { + when (action) { + MessageActionsAction.ToggleReportMenu -> toggleReportMenu() + } + } + + private fun toggleReportMenu() = withState { setState { copy( expendedReportContentMenu = it.expendedReportContentMenu.not() @@ -183,63 +189,63 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } - private fun actionsForEvent(optionalEvent: Optional): List { + private fun actionsForEvent(optionalEvent: Optional): List { val event = optionalEvent.getOrNull() ?: return emptyList() val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel() ?: event.root.getClearContent().toModel() val type = messageContent?.type - return arrayListOf().apply { + return arrayListOf().apply { if (event.root.sendState.hasFailed()) { if (canRetry(event)) { - add(SimpleAction.Resend(eventId)) + add(EventSharedAction.Resend(eventId)) } - add(SimpleAction.Remove(eventId)) + add(EventSharedAction.Remove(eventId)) } else if (event.root.sendState.isSending()) { // TODO is uploading attachment? if (canCancel(event)) { - add(SimpleAction.Cancel(eventId)) + add(EventSharedAction.Cancel(eventId)) } } else if (event.root.sendState == SendState.SYNCED) { if (!event.root.isRedacted()) { if (canReply(event, messageContent)) { - add(SimpleAction.Reply(eventId)) + add(EventSharedAction.Reply(eventId)) } if (canEdit(event, session.myUserId)) { - add(SimpleAction.Edit(eventId)) + add(EventSharedAction.Edit(eventId)) } if (canRedact(event, session.myUserId)) { - add(SimpleAction.Delete(eventId)) + add(EventSharedAction.Delete(eventId)) } if (canCopy(type)) { // TODO copy images? html? see ClipBoard - add(SimpleAction.Copy(messageContent!!.body)) + add(EventSharedAction.Copy(messageContent!!.body)) } if (event.canReact()) { - add(SimpleAction.AddReaction(eventId)) + add(EventSharedAction.AddReaction(eventId)) } if (canQuote(event, messageContent)) { - add(SimpleAction.Quote(eventId)) + add(EventSharedAction.Quote(eventId)) } if (canViewReactions(event)) { - add(SimpleAction.ViewReactions(informationData)) + add(EventSharedAction.ViewReactions(informationData)) } if (event.hasBeenEdited()) { - add(SimpleAction.ViewEditHistory(informationData)) + add(EventSharedAction.ViewEditHistory(informationData)) } if (canShare(type)) { if (messageContent is MessageImageContent) { session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url -> - add(SimpleAction.Share(url)) + add(EventSharedAction.Share(url)) } } // TODO @@ -252,17 +258,22 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } - add(SimpleAction.ViewSource(event.root.toContentStringWithIndent())) + add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent())) if (event.isEncrypted()) { val decryptedContent = event.root.toClearContentStringWithIndent() ?: stringProvider.getString(R.string.encryption_information_decryption_error) - add(SimpleAction.ViewDecryptedSource(decryptedContent)) + add(EventSharedAction.ViewDecryptedSource(decryptedContent)) } - add(SimpleAction.CopyPermalink(eventId)) + add(EventSharedAction.CopyPermalink(eventId)) - if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) { + if (session.myUserId != event.root.senderId) { // not sent by me - add(SimpleAction.ReportContent(eventId)) + if (event.root.getClearType() == EventType.MESSAGE) { + add(EventSharedAction.ReportContent(eventId, event.root.senderId)) + } + + add(EventSharedAction.Separator) + add(EventSharedAction.IgnoreUser(event.root.senderId)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageSharedActionViewModel.kt new file mode 100644 index 0000000000..2e041fd2ea --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageSharedActionViewModel.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.home.room.detail.timeline.action + +import im.vector.riotx.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +/** + * Activity shared view model to handle message actions + */ +class MessageSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt deleted file mode 100644 index 5da589d862..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/SimpleAction.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.features.home.room.detail.timeline.action - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import im.vector.riotx.R -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData - -sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) { - data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction) - data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy) - data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit) - data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote) - data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply) - data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share) - data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw) - data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash) - data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete) - data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round) - data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source) - data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source) - data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink) - data class ReportContent(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag) - data class ReportContentSpam(val eventId: String) : SimpleAction(R.string.report_content_spam, R.drawable.ic_report_spam) - data class ReportContentInappropriate(val eventId: String) : SimpleAction(R.string.report_content_inappropriate, R.drawable.ic_report_inappropriate) - data class ReportContentCustom(val eventId: String) : SimpleAction(R.string.report_content_custom, R.drawable.ic_report_custom) - data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0) - data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions) - data class ViewEditHistory(val messageInformationData: MessageInformationData) : - SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history) -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index d36e98f67c..4661d8f8cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -38,7 +38,7 @@ import im.vector.riotx.features.html.EventHtmlRenderer import me.gujun.android.span.span import name.fraser.neil.plaintext.diff_match_patch import timber.log.Timber -import java.util.* +import java.util.Calendar /** * Epoxy controller for edit history list @@ -94,7 +94,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, val body = cContent.second?.let { eventHtmlRenderer.render(it) } ?: cContent.first - val nextEvent = if (index + 1 <= sourceEvents.lastIndex) sourceEvents[index + 1] else null + val nextEvent = sourceEvents.getOrNull(index + 1) var spannedDiff: Spannable? = null if (nextEvent != null && cContent.second == null /*No diff for html*/) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt index e2b976b273..c1cccbef7a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt @@ -26,8 +26,9 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult -import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import timber.log.Timber import java.util.* @@ -46,7 +47,7 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted initialState: ViewEditHistoryViewState, val session: Session, val dateFormatter: VectorDateFormatter -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { private val roomId = initialState.roomId private val eventId = initialState.eventId @@ -115,4 +116,8 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted } }) } + + override fun handle(action: EmptyAction) { + // No op + } } 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 12f49c2e74..3f234fcd3e 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 @@ -27,8 +27,6 @@ import im.vector.riotx.core.resources.StringProvider 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.AvatarSizeProvider -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_ @@ -41,13 +39,13 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri fun create(event: TimelineEvent, highlight: Boolean, callback: TimelineEventController.Callback?): NoticeItem? { - val text = buildNoticeText(event.root, event.senderName) ?: return null + val text = buildNoticeText(event.root, event.getDisambiguatedDisplayName()) ?: return null val informationData = MessageInformationData( eventId = event.root.eventId ?: "?", senderId = event.root.senderId ?: "", sendState = event.root.sendState, - avatarUrl = event.senderAvatar(), - memberName = event.senderName(), + avatarUrl = event.senderAvatar, + memberName = event.getDisambiguatedDisplayName(), showInformation = false ) val attributes = NoticeItem.Attributes( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 1df885cd35..51364e24c9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -64,12 +64,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act if (!showReadMarker && mergedEvent.hasReadMarker && readMarkerVisible) { showReadMarker = true } - val senderAvatar = mergedEvent.senderAvatar() - val senderName = mergedEvent.senderName() + val senderAvatar = mergedEvent.senderAvatar + val senderName = mergedEvent.getDisambiguatedDisplayName() val data = MergedHeaderItem.Data( userId = mergedEvent.root.senderId ?: "", avatarUrl = senderAvatar, - memberName = senderName ?: "", + memberName = senderName, localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "" ) 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 0bb5c3a1d8..ac6c563099 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 @@ -46,9 +46,11 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle import im.vector.riotx.features.home.room.detail.timeline.helper.* import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.html.EventHtmlRenderer +import im.vector.riotx.features.html.CodeVisitor import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import me.gujun.android.span.span +import org.commonmark.node.Document import javax.inject.Inject class MessageItemFactory @Inject constructor( @@ -87,7 +89,7 @@ class MessageItemFactory @Inject constructor( return defaultItemFactory.create(malformedText, informationData, highlight, callback) } if (messageContent.relatesTo?.type == RelationType.REPLACE - || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE + || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should it when debugging as a notice event return noticeItemFactory.create(event, highlight, readMarkerVisible, callback) @@ -97,22 +99,14 @@ class MessageItemFactory @Inject constructor( // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, - informationData, - highlight, - callback, - attributes) - is MessageTextContent -> buildTextMessageItem(messageContent, - informationData, - highlight, - callback, - attributes) - is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) - else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback) + is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) + is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) + else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback) } } @@ -163,7 +157,7 @@ class MessageItemFactory @Inject constructor( return defaultItemFactory.create(text, informationData, highlight, callback) } - private fun buildImageMessageItem(messageContent: MessageImageContent, + private fun buildImageMessageItem(messageContent: MessageImageInfoContent, @Suppress("UNUSED_PARAMETER") informationData: MessageInformationData, highlight: Boolean, @@ -202,7 +196,7 @@ class MessageItemFactory @Inject constructor( val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, url = messageContent.videoInfo?.thumbnailFile?.url - ?: messageContent.videoInfo?.thumbnailUrl, + ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight, @@ -229,34 +223,75 @@ class MessageItemFactory @Inject constructor( .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } } - private fun buildTextMessageItem(messageContent: MessageTextContent, + private fun buildItemForTextContent(messageContent: MessageTextContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { + val isFormatted = messageContent.formattedBody.isNullOrBlank().not() + return if (isFormatted) { + val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document + val codeVisitor = CodeVisitor() + codeVisitor.visit(localFormattedBody) + when (codeVisitor.codeKind) { + CodeVisitor.Kind.BLOCK -> { + val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody) + buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes) + } + CodeVisitor.Kind.INLINE -> { + val codeFormatted = htmlRenderer.get().render(localFormattedBody) + buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) + } + CodeVisitor.Kind.NONE -> { + val formattedBody = htmlRenderer.get().render(messageContent.formattedBody!!) + buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes) + } + } + } else { + buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) + } + } + + private fun buildMessageTextItem(body: CharSequence, + isFormatted: Boolean, informationData: MessageInformationData, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { - val isFormatted = messageContent.formattedBody.isNullOrBlank().not() - val bodyToUse = messageContent.formattedBody?.let { - htmlRenderer.get().render(it.trim()) - } ?: messageContent.body + val linkifiedBody = linkifyBody(body, callback) - val linkifiedBody = linkifyBody(bodyToUse, callback) - - return MessageTextItem_() - .apply { - if (informationData.hasBeenEdited) { - val spannable = annotateWithEdited(linkifiedBody, callback, informationData) - message(spannable) - } else { - message(linkifiedBody) - } - } + return MessageTextItem_().apply { + if (informationData.hasBeenEdited) { + val spannable = annotateWithEdited(linkifiedBody, callback, informationData) + message(spannable) + } else { + message(linkifiedBody) + } + } .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .searchForPills(isFormatted) .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) .urlClickCallback(callback) - // click on the text + } + + private fun buildCodeBlockItem(formattedBody: CharSequence, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageBlockCodeItem? { + return MessageBlockCodeItem_() + .apply { + if (informationData.hasBeenEdited) { + val spannable = annotateWithEdited("", callback, informationData) + editedSpan(spannable) + } + } + .leftGuideline(avatarSizeProvider.leftGuideline) + .attributes(attributes) + .highlighted(highlight) + .message(formattedBody) } private fun annotateWithEdited(linkifiedBody: CharSequence, @@ -291,9 +326,9 @@ class MessageItemFactory @Inject constructor( // nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index b396eb1f0e..618ca121c2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -39,23 +39,25 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me val computedModel = try { when (event.root.getClearType()) { - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) + EventType.STICKER, + EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) // State and call EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, + EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, EventType.REACTION, EventType.REDACTION, - EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, readMarkerVisible, callback) + EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, readMarkerVisible, callback) // State room create - EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) + EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto - EventType.ENCRYPTED -> { + EventType.ENCRYPTED -> { if (event.root.isRedacted()) { // Redacted event, let the MessageItemFactory handle it messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback) @@ -65,9 +67,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me } // Unhandled event types (yet) - EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER -> defaultItemFactory.create(event, highlight, readMarkerVisible, callback) - else -> { + EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, readMarkerVisible, callback) + else -> { Timber.v("Type ${event.root.getClearType()} not handled") null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 2d116e4a90..a3910664a2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -19,13 +19,19 @@ package im.vector.riotx.features.home.room.detail.timeline.format 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 -import im.vector.matrix.android.api.session.room.model.* +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility +import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent +import im.vector.matrix.android.api.session.room.model.RoomJoinRules +import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomNameContent +import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.api.session.room.model.call.CallInviteContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.home.room.detail.timeline.helper.senderName import timber.log.Timber import javax.inject.Inject @@ -34,9 +40,10 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active fun format(timelineEvent: TimelineEvent): CharSequence? { return when (val type = timelineEvent.root.getClearType()) { + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderName()) + EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_INVITE, @@ -54,6 +61,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active fun format(event: Event, senderName: String?): CharSequence? { return when (val type = event.getClearType()) { + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName) @@ -96,7 +104,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active } private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? { - val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null + val historyVisibility = event.getClearContent().toModel()?.historyVisibility + ?: return null val formattedVisibility = when (historyVisibility) { RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) @@ -135,7 +144,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active } } - private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { + private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String { val displayText = StringBuilder() // Check display name has been changed if (eventContent?.displayName != prevEventContent?.displayName) { @@ -146,7 +155,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName) else -> stringProvider.getString(R.string.notice_display_name_changed_from, - event.senderId, prevEventContent?.displayName, eventContent?.displayName) + event.senderId, prevEventContent?.displayName, eventContent?.displayName) } displayText.append(displayNameText) } @@ -160,6 +169,11 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active } displayText.append(displayAvatarText) } + if (displayText.isEmpty()) { + displayText.append( + stringProvider.getString(R.string.notice_member_no_changes, senderName) + ) + } return displayText.toString() } @@ -171,9 +185,10 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId when { eventContent.thirdPartyInvite != null -> { - val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey + val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid + ?: event.stateKey stringProvider.getString(R.string.notice_room_third_party_registered_invite, - userWhoHasAccepted, eventContent.thirdPartyInvite?.displayName) + userWhoHasAccepted, eventContent.thirdPartyInvite?.displayName) } event.stateKey == selfUserId -> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName) @@ -209,4 +224,13 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active else -> null } } + + private fun formatJoinRulesEvent(event: Event, senderName: String?): CharSequence? { + val content = event.getClearContent().toModel() ?: return null + return when (content.joinRules) { + RoomJoinRules.INVITE -> stringProvider.getString(R.string.room_join_rules_invite, senderName) + RoomJoinRules.PUBLIC -> stringProvider.getString(R.string.room_join_rules_public, senderName) + else -> null + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index a75ac86c1b..1cd851f8c8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -17,8 +17,6 @@ package im.vector.riotx.features.home.room.detail.timeline.helper 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.RoomMember import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.extensions.localDateTime @@ -38,7 +36,8 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STICKER, EventType.STATE_ROOM_CREATE, - EventType.STATE_ROOM_TOMBSTONE + EventType.STATE_ROOM_TOMBSTONE, + EventType.STATE_ROOM_JOIN_RULES ) val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf( @@ -47,25 +46,6 @@ object TimelineDisplayableEvents { ) } -fun TimelineEvent.senderAvatar(): String? { - // We might have no avatar when user leave, so we try to get it from prevContent - return senderAvatar - ?: if (root.type == EventType.STATE_ROOM_MEMBER) { - root.prevContent.toModel()?.avatarUrl - } else { - null - } -} - -fun TimelineEvent.senderName(): String? { - // We might have no senderName when user leave, so we try to get it from prevContent - return when { - senderName != null -> getDisambiguatedDisplayName() - root.type == EventType.STATE_ROOM_MEMBER -> root.prevContent.toModel()?.displayName - else -> null - } -} - fun TimelineEvent.canBeMerged(): Boolean { return root.getClearType() == EventType.STATE_ROOM_MEMBER } 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 bddee50861..2ca6bbfd37 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 @@ -139,6 +139,7 @@ abstract class AbsMessageItem : BaseEventItem() { override fun unbind(holder: H) { holder.readMarkerView.unbind() + holder.readReceiptsView.unbind() super.unbind(holder) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt new file mode 100644 index 0000000000..82a6a4db6f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.timeline.item + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.extensions.setTextOrHide +import me.saket.bettermovementmethod.BetterLinkMovementMethod + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +abstract class MessageBlockCodeItem : AbsMessageItem() { + + @EpoxyAttribute + var message: CharSequence? = null + @EpoxyAttribute + var editedSpan: CharSequence? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.messageView.text = message + renderSendState(holder.messageView, holder.messageView) + holder.messageView.setOnClickListener(attributes.itemClickListener) + holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) + holder.editedView.movementMethod = BetterLinkMovementMethod.getInstance() + holder.editedView.setTextOrHide(editedSpan) + } + + override fun getViewType() = STUB_ID + + class Holder : AbsMessageItem.Holder(STUB_ID) { + val messageView by bind(R.id.codeBlockTextView) + val editedView by bind(R.id.codeBlockEditedView) + } + + companion object { + private const val STUB_ID = R.id.messageContentCodeBlockStub + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 928a18fe4d..457f30cbf4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -24,6 +24,7 @@ import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R +import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.riotx.features.media.ImageContentRenderer @@ -60,6 +61,7 @@ abstract class MessageImageVideoItem : AbsMessageItem() { private val mvmtMethod = BetterLinkMovementMethod.newInstance().also { it.setOnLinkClickListener { _, url -> // Return false to let android manage the click on the link, or true if the link is handled by the application - urlClickCallback?.onUrlClicked(url) == true + url.isValidUrl() && urlClickCallback?.onUrlClicked(url) == true } // We need also to fix the case when long click on link will trigger long click on cell it.setOnLinkLongClickListener { tv, url -> // Long clicks are handled by parent, return true to block android to do something with url - if (urlClickCallback?.onUrlLongClicked(url) == true) { + if (url.isValidUrl() && urlClickCallback?.onUrlLongClicked(url) == true) { tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)) true } else { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt index 208e126022..9ec45b03b9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt @@ -16,20 +16,16 @@ package im.vector.riotx.features.home.room.detail.timeline.reactions -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary import im.vector.matrix.rx.RxRoom import im.vector.matrix.rx.unwrap -import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import io.reactivex.Observable import io.reactivex.Single @@ -55,15 +51,15 @@ data class ReactionInfo( * Used to display the list of members that reacted to a given event */ class ViewReactionsViewModel @AssistedInject constructor(@Assisted - initialState: DisplayReactionsViewState, + initialState: DisplayReactionsViewState, private val session: Session, private val dateFormatter: VectorDateFormatter -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { private val roomId = initialState.roomId private val eventId = initialState.eventId private val room = session.getRoom(roomId) - ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") + ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") @AssistedInject.Factory interface Factory { @@ -103,7 +99,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted .fromIterable(summary.sourceEvents) .map { val event = room.getTimeLineEvent(it) - ?: throw RuntimeException("Your eventId is not valid") + ?: throw RuntimeException("Your eventId is not valid") ReactionInfo( event.root.eventId!!, summary.key, @@ -115,4 +111,8 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted } }.toList() } + + override fun handle(action: EmptyAction) { + // No op + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt index 82fd203b87..c4bb0d9b15 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt @@ -24,13 +24,17 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.home.RoomListDisplayMode import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListParams import kotlinx.android.synthetic.main.activity_filtered_rooms.* class FilteredRoomsActivity : VectorBaseActivity() { - private lateinit var roomListFragment: RoomListFragment + private val roomListFragment: RoomListFragment? + get() { + return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomListFragment + } override fun getLayoutRes(): Int { return R.layout.activity_filtered_rooms @@ -44,19 +48,16 @@ class FilteredRoomsActivity : VectorBaseActivity() { super.onCreate(savedInstanceState) configureToolbar(filteredRoomsToolbar) if (isFirstCreation()) { - roomListFragment = RoomListFragment.newInstance(RoomListParams(RoomListFragment.DisplayMode.FILTERED)) - replaceFragment(roomListFragment, R.id.filteredRoomsFragmentContainer, FRAGMENT_TAG) - } else { - roomListFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as RoomListFragment + val params = RoomListParams(RoomListDisplayMode.FILTERED) + replaceFragment(R.id.filteredRoomsFragmentContainer, RoomListFragment::class.java, params, FRAGMENT_TAG) } - filteredRoomsSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { return true } override fun onQueryTextChange(newText: String): Boolean { - roomListFragment.filterRoomsWith(newText) + roomListFragment?.filterRoomsWith(newText) return true } }) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt similarity index 63% rename from vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt index 8271086421..9db7374169 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt @@ -17,12 +17,16 @@ package im.vector.riotx.features.home.room.list import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.notification.RoomNotificationState +import im.vector.riotx.core.platform.VectorViewModelAction -sealed class RoomListActions { - data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions() - data class ToggleCategory(val category: RoomCategory) : RoomListActions() - data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListActions() - data class RejectInvitation(val roomSummary: RoomSummary) : RoomListActions() - data class FilterWith(val filter: String) : RoomListActions() - object MarkAllRoomsRead : RoomListActions() +sealed class RoomListAction : VectorViewModelAction { + data class SelectRoom(val roomSummary: RoomSummary) : RoomListAction() + data class ToggleCategory(val category: RoomCategory) : RoomListAction() + data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListAction() + data class RejectInvitation(val roomSummary: RoomSummary) : RoomListAction() + data class FilterWith(val filter: String) : RoomListAction() + data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction() + data class LeaveRoom(val roomId: String) : RoomListAction() + object MarkAllRoomsRead : RoomListAction() } 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 3ea6745a94..8cae1fd4e8 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 @@ -18,21 +18,22 @@ package im.vector.riotx.features.home.room.list import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.features.home.RoomListDisplayMode import io.reactivex.functions.Predicate -class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.DisplayMode) : Predicate { +class RoomListDisplayModeFilter(private val displayMode: RoomListDisplayMode) : Predicate { override fun test(roomSummary: RoomSummary): Boolean { if (roomSummary.membership.isLeft()) { return false } return when (displayMode) { - RoomListFragment.DisplayMode.HOME -> + RoomListDisplayMode.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 - RoomListFragment.DisplayMode.SHARE -> roomSummary.membership == Membership.JOIN + RoomListDisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN + RoomListDisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN + RoomListDisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN + RoomListDisplayMode.SHARE -> roomSummary.membership == Membership.JOIN } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index a705c91a9e..a5e9a7b4bf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -20,7 +20,8 @@ import android.os.Bundle import android.os.Parcelable import android.view.Menu import android.view.MenuItem -import androidx.annotation.StringRes +import android.view.View +import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager @@ -30,59 +31,46 @@ import com.google.android.material.snackbar.Snackbar import im.vector.matrix.android.api.failure.Failure 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.notification.RoomNotificationState import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.error.ErrorFormatter -import im.vector.riotx.core.extensions.observeEvent -import im.vector.riotx.core.extensions.observeEventFirstThrottle import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.VectorBaseFragment + +import im.vector.riotx.features.home.RoomListDisplayMode +import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction +import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet +import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.riotx.features.home.room.list.widget.FabMenuView import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.share.SharedData +import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_list.* import javax.inject.Inject @Parcelize data class RoomListParams( - val displayMode: RoomListFragment.DisplayMode, + val displayMode: RoomListDisplayMode, val sharedData: SharedData? = null ) : Parcelable -class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener { +class RoomListFragment @Inject constructor( + private val roomController: RoomSummaryController, + val roomListViewModelFactory: RoomListViewModel.Factory, + private val errorFormatter: ErrorFormatter, + private val notificationDrawerManager: NotificationDrawerManager - enum class DisplayMode(@StringRes val titleRes: Int) { - HOME(R.string.bottom_action_home), - PEOPLE(R.string.bottom_action_people_x), - ROOMS(R.string.bottom_action_rooms), - FILTERED(/* Not used */ 0), - SHARE(/* Not used */ 0) - } - - companion object { - fun newInstance(roomListParams: RoomListParams): RoomListFragment { - return RoomListFragment().apply { - setArguments(roomListParams) - } - } - } +) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener { + private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel private val roomListParams: RoomListParams by args() - @Inject lateinit var roomController: RoomSummaryController - @Inject lateinit var roomListViewModelFactory: RoomListViewModel.Factory - @Inject lateinit var errorFormatter: ErrorFormatter - @Inject lateinit var notificationDrawerManager: NotificationDrawerManager private val roomListViewModel: RoomListViewModel by fragmentViewModel() override fun getLayoutResId() = R.layout.fragment_room_list - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - private var hasUnreadRooms = false override fun getMenuRes() = R.menu.room_list @@ -90,7 +78,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_home_mark_all_as_read -> { - roomListViewModel.accept(RoomListActions.MarkAllRoomsRead) + roomListViewModel.handle(RoomListAction.MarkAllRoomsRead) return true } } @@ -103,36 +91,59 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O super.onPrepareOptionsMenu(menu) } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) setupCreateRoomButton() setupRecyclerView() + sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) + roomListViewModel.subscribe { renderState(it) } - roomListViewModel.openRoomLiveData.observeEventFirstThrottle(this, 800L) { - if (roomListParams.displayMode == DisplayMode.SHARE) { - val sharedData = roomListParams.sharedData ?: return@observeEventFirstThrottle - navigator.openRoomForSharing(requireActivity(), it, sharedData) - } else { - navigator.openRoom(requireActivity(), it) - } - } + roomListViewModel.viewEvents + .observe() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + when (it) { + is RoomListViewEvents.SelectRoom -> openSelectedRoom(it) + is RoomListViewEvents.Failure -> showError(it) + } + } + .disposeOnDestroyView() createChatFabMenu.listener = this - roomListViewModel.invitationAnswerErrorLiveData.observeEvent(this) { throwable -> - vectorBaseActivity.coordinatorLayout?.let { - Snackbar.make(it, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT) - .show() - } + sharedActionViewModel + .observe() + .subscribe { handleQuickActions(it) } + .disposeOnDestroyView() + } + + override fun onDestroyView() { + super.onDestroyView() + roomListView.adapter = null + } + + private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) { + if (roomListParams.displayMode == RoomListDisplayMode.SHARE) { + val sharedData = roomListParams.sharedData ?: return + navigator.openRoomForSharing(requireActivity(), event.roomId, sharedData) + } else { + navigator.openRoom(requireActivity(), event.roomId) + } + } + + private fun showError(event: RoomListViewEvents.Failure) { + vectorBaseActivity.coordinatorLayout?.let { + Snackbar.make(it, errorFormatter.toHumanReadable(event.throwable), Snackbar.LENGTH_SHORT) + .show() } } private fun setupCreateRoomButton() { when (roomListParams.displayMode) { - DisplayMode.HOME -> createChatFabMenu.isVisible = true - DisplayMode.PEOPLE -> createChatRoomButton.isVisible = true - DisplayMode.ROOMS -> createGroupRoomButton.isVisible = true - else -> Unit // No button in this mode + RoomListDisplayMode.HOME -> createChatFabMenu.isVisible = true + RoomListDisplayMode.PEOPLE -> createChatRoomButton.isVisible = true + RoomListDisplayMode.ROOMS -> createGroupRoomButton.isVisible = true + else -> Unit // No button in this mode } createChatRoomButton.setOnClickListener { @@ -143,7 +154,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O } // Hide FAB when list is scrolling - roomListEpoxyRecyclerView.addOnScrollListener( + roomListView.addOnScrollListener( object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { createChatFabMenu.removeCallbacks(showFabRunnable) @@ -155,10 +166,10 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O RecyclerView.SCROLL_STATE_DRAGGING, RecyclerView.SCROLL_STATE_SETTLING -> { when (roomListParams.displayMode) { - DisplayMode.HOME -> createChatFabMenu.hide() - DisplayMode.PEOPLE -> createChatRoomButton.hide() - DisplayMode.ROOMS -> createGroupRoomButton.hide() - else -> Unit + RoomListDisplayMode.HOME -> createChatFabMenu.hide() + RoomListDisplayMode.PEOPLE -> createChatRoomButton.hide() + RoomListDisplayMode.ROOMS -> createGroupRoomButton.hide() + else -> Unit } } } @@ -168,9 +179,9 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O fun filterRoomsWith(filter: String) { // Scroll the list to top - roomListEpoxyRecyclerView.scrollToPosition(0) + roomListView.scrollToPosition(0) - roomListViewModel.accept(RoomListActions.FilterWith(filter)) + roomListViewModel.handle(RoomListAction.FilterWith(filter)) } override fun openRoomDirectory(initialFilter: String) { @@ -184,21 +195,51 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O private fun setupRecyclerView() { val layoutManager = LinearLayoutManager(context) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() - roomListEpoxyRecyclerView.layoutManager = layoutManager - roomListEpoxyRecyclerView.itemAnimator = RoomListAnimator() + roomListView.layoutManager = layoutManager + roomListView.itemAnimator = RoomListAnimator() roomController.listener = this roomController.addModelBuildListener { it.dispatchTo(stateRestorer) } - stateView.contentView = roomListEpoxyRecyclerView - roomListEpoxyRecyclerView.setController(roomController) + roomListView.adapter = roomController.adapter + stateView.contentView = roomListView } private val showFabRunnable = Runnable { if (isAdded) { when (roomListParams.displayMode) { - DisplayMode.HOME -> createChatFabMenu.show() - DisplayMode.PEOPLE -> createChatRoomButton.show() - DisplayMode.ROOMS -> createGroupRoomButton.show() - else -> Unit + RoomListDisplayMode.HOME -> createChatFabMenu.show() + RoomListDisplayMode.PEOPLE -> createChatRoomButton.show() + RoomListDisplayMode.ROOMS -> createGroupRoomButton.show() + else -> Unit + } + } + } + + private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) { + when (quickAction) { + is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> { + roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY)) + } + is RoomListQuickActionsSharedAction.NotificationsAll -> { + roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES)) + } + is RoomListQuickActionsSharedAction.NotificationsMentionsOnly -> { + roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MENTIONS_ONLY)) + } + is RoomListQuickActionsSharedAction.NotificationsMute -> { + roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MUTE)) + } + is RoomListQuickActionsSharedAction.Settings -> { + vectorBaseActivity.notImplemented("Opening room settings") + } + is RoomListQuickActionsSharedAction.Leave -> { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.room_participants_leave_prompt_title) + .setMessage(R.string.room_participants_leave_prompt_msg) + .setPositiveButton(R.string.leave) { _, _ -> + roomListViewModel.handle(RoomListAction.LeaveRoom(quickAction.roomId)) + } + .setNegativeButton(R.string.cancel, null) + .show() } } } @@ -213,16 +254,16 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O // Mark all as read menu when (roomListParams.displayMode) { - DisplayMode.HOME, - DisplayMode.PEOPLE, - DisplayMode.ROOMS -> { + RoomListDisplayMode.HOME, + RoomListDisplayMode.PEOPLE, + RoomListDisplayMode.ROOMS -> { val newValue = state.hasUnread if (hasUnreadRooms != newValue) { hasUnreadRooms = newValue requireActivity().invalidateOptionsMenu() } } - else -> Unit + else -> Unit } } @@ -243,7 +284,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O } .isNullOrEmpty() val emptyState = when (roomListParams.displayMode) { - DisplayMode.HOME -> { + RoomListDisplayMode.HOME -> { if (hasNoRoom) { StateView.State.Empty( getString(R.string.room_list_catchup_welcome_title), @@ -257,19 +298,19 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O getString(R.string.room_list_catchup_empty_body)) } } - DisplayMode.PEOPLE -> + RoomListDisplayMode.PEOPLE -> StateView.State.Empty( getString(R.string.room_list_people_empty_title), ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_chat), getString(R.string.room_list_people_empty_body) ) - DisplayMode.ROOMS -> + RoomListDisplayMode.ROOMS -> StateView.State.Empty( getString(R.string.room_list_rooms_empty_title), ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group), getString(R.string.room_list_rooms_empty_body) ) - else -> + else -> // Always display the content in this mode, because if the footer StateView.State.Content } @@ -298,22 +339,31 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O // RoomSummaryController.Callback ************************************************************** - override fun onRoomSelected(room: RoomSummary) { - roomListViewModel.accept(RoomListActions.SelectRoom(room)) + override fun onRoomClicked(room: RoomSummary) { + roomListViewModel.handle(RoomListAction.SelectRoom(room)) + } + + override fun onRoomLongClicked(room: RoomSummary): Boolean { + roomController.onRoomLongClicked() + + RoomListQuickActionsBottomSheet + .newInstance(room.roomId) + .show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS") + return true } override fun onAcceptRoomInvitation(room: RoomSummary) { notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId) - roomListViewModel.accept(RoomListActions.AcceptInvitation(room)) + roomListViewModel.handle(RoomListAction.AcceptInvitation(room)) } override fun onRejectRoomInvitation(room: RoomSummary) { notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId) - roomListViewModel.accept(RoomListActions.RejectInvitation(room)) + roomListViewModel.handle(RoomListAction.RejectInvitation(room)) } override fun onToggleRoomCategory(roomCategory: RoomCategory) { - roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory)) + roomListViewModel.handle(RoomListAction.ToggleCategory(roomCategory)) } override fun createRoom(initialName: String) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt new file mode 100644 index 0000000000..1181236da2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.features.home.room.list + +/** + * Transient events for RoomList + */ +sealed class RoomListViewEvents { + data class Failure(val throwable: Throwable) : RoomListViewEvents() + data class SelectRoom(val roomId: String) : RoomListViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index c413a09c8a..e5924d9f2a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -16,8 +16,6 @@ package im.vector.riotx.features.home.room.list -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext @@ -26,20 +24,17 @@ 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.RoomSummary import im.vector.matrix.android.api.session.room.model.tag.RoomTag -import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.core.utils.LiveEvent -import im.vector.riotx.core.utils.RxStore +import im.vector.riotx.core.utils.DataSource +import im.vector.riotx.core.utils.PublishDataSource import io.reactivex.schedulers.Schedulers import timber.log.Timber import javax.inject.Inject class RoomListViewModel @Inject constructor(initialState: RoomListViewState, private val session: Session, - private val roomSummariesStore: RxStore>, - private val alphabeticalRoomComparator: AlphabeticalRoomComparator, - private val chronologicalRoomComparator: ChronologicalRoomComparator) - : VectorViewModel(initialState) { + private val roomSummariesSource: DataSource>) + : VectorViewModel(initialState) { interface Factory { fun create(initialState: RoomListViewState): RoomListViewModel @@ -57,40 +52,37 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, private val displayMode = initialState.displayMode private val roomListDisplayModeFilter = RoomListDisplayModeFilter(displayMode) - private val _openRoomLiveData = MutableLiveData>() - val openRoomLiveData: LiveData> - get() = _openRoomLiveData - - private val _invitationAnswerErrorLiveData = MutableLiveData>() - val invitationAnswerErrorLiveData: LiveData> - get() = _invitationAnswerErrorLiveData + private val _viewEvents = PublishDataSource() + val viewEvents: DataSource = _viewEvents init { observeRoomSummaries() } - fun accept(action: RoomListActions) { + override fun handle(action: RoomListAction) { when (action) { - is RoomListActions.SelectRoom -> handleSelectRoom(action) - is RoomListActions.ToggleCategory -> handleToggleCategory(action) - is RoomListActions.AcceptInvitation -> handleAcceptInvitation(action) - is RoomListActions.RejectInvitation -> handleRejectInvitation(action) - is RoomListActions.FilterWith -> handleFilter(action) - is RoomListActions.MarkAllRoomsRead -> handleMarkAllRoomsRead() + is RoomListAction.SelectRoom -> handleSelectRoom(action) + is RoomListAction.ToggleCategory -> handleToggleCategory(action) + is RoomListAction.AcceptInvitation -> handleAcceptInvitation(action) + is RoomListAction.RejectInvitation -> handleRejectInvitation(action) + is RoomListAction.FilterWith -> handleFilter(action) + is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() + is RoomListAction.LeaveRoom -> handleLeaveRoom(action) + is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) } } // PRIVATE METHODS ***************************************************************************** - private fun handleSelectRoom(action: RoomListActions.SelectRoom) { - _openRoomLiveData.postLiveEvent(action.roomSummary.roomId) + private fun handleSelectRoom(action: RoomListAction.SelectRoom) { + _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary.roomId)) } - private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState { + private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState { this.toggle(action.category) } - private fun handleFilter(action: RoomListActions.FilterWith) { + private fun handleFilter(action: RoomListAction.FilterWith) { setState { copy( roomFilter = action.filter @@ -99,17 +91,14 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, } private fun observeRoomSummaries() { - roomSummariesStore + roomSummariesSource .observe() .observeOn(Schedulers.computation()) - .map { - it.sortedWith(chronologicalRoomComparator) - } .execute { asyncRooms -> copy(asyncRooms = asyncRooms) } - roomSummariesStore + roomSummariesSource .observe() .observeOn(Schedulers.computation()) .map { buildRoomSummaries(it) } @@ -118,7 +107,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, } } - private fun handleAcceptInvitation(action: RoomListActions.AcceptInvitation) = withState { state -> + private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state -> val roomId = action.roomSummary.roomId if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) { @@ -142,8 +131,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, override fun onFailure(failure: Throwable) { // Notify the user - _invitationAnswerErrorLiveData.postLiveEvent(failure) - + _viewEvents.post(RoomListViewEvents.Failure(failure)) setState { copy( joiningRoomsIds = joiningRoomsIds - roomId, @@ -154,7 +142,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, }) } - private fun handleRejectInvitation(action: RoomListActions.RejectInvitation) = withState { state -> + private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state -> val roomId = action.roomSummary.roomId if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) { @@ -180,8 +168,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, override fun onFailure(failure: Throwable) { // Notify the user - _invitationAnswerErrorLiveData.postLiveEvent(failure) - + _viewEvents.post(RoomListViewEvents.Failure(failure)) setState { copy( rejectingRoomsIds = rejectingRoomsIds - roomId, @@ -201,11 +188,28 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, ?.let { session.markAllAsRead(it, object : MatrixCallback {}) } } + private fun handleChangeNotificationMode(action: RoomListAction.ChangeRoomNotificationState) { + session.getRoom(action.roomId)?.setRoomNotificationState(action.notificationState, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + _viewEvents.post(RoomListViewEvents.Failure(failure)) + } + }) + } + + private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) { + session.getRoom(action.roomId)?.leave(object : MatrixCallback { + override fun onFailure(failure: Throwable) { + _viewEvents.post(RoomListViewEvents.Failure(failure)) + } + }) + } + private fun buildRoomSummaries(rooms: List): RoomSummaries { + // Set up init size on directChats and groupRooms as they are the biggest ones val invites = ArrayList() val favourites = ArrayList() - val directChats = ArrayList() - val groupRooms = ArrayList() + val directChats = ArrayList(rooms.size) + val groupRooms = ArrayList(rooms.size) val lowPriorities = ArrayList() val serverNotices = ArrayList() @@ -223,21 +227,13 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, } } - val roomComparator = when (displayMode) { - RoomListFragment.DisplayMode.HOME -> chronologicalRoomComparator - RoomListFragment.DisplayMode.PEOPLE -> chronologicalRoomComparator - RoomListFragment.DisplayMode.ROOMS -> chronologicalRoomComparator - RoomListFragment.DisplayMode.FILTERED -> chronologicalRoomComparator - RoomListFragment.DisplayMode.SHARE -> chronologicalRoomComparator - } - return RoomSummaries().apply { - put(RoomCategory.INVITE, invites.sortedWith(roomComparator)) - put(RoomCategory.FAVOURITE, favourites.sortedWith(roomComparator)) - put(RoomCategory.DIRECT, directChats.sortedWith(roomComparator)) - put(RoomCategory.GROUP, groupRooms.sortedWith(roomComparator)) - put(RoomCategory.LOW_PRIORITY, lowPriorities.sortedWith(roomComparator)) - put(RoomCategory.SERVER_NOTICE, serverNotices.sortedWith(roomComparator)) + put(RoomCategory.INVITE, invites) + put(RoomCategory.FAVOURITE, favourites) + put(RoomCategory.DIRECT, directChats) + put(RoomCategory.GROUP, groupRooms) + put(RoomCategory.LOW_PRIORITY, lowPriorities) + put(RoomCategory.SERVER_NOTICE, serverNotices) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt index 5895aa4e52..60ec92d8cf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModelFactory.kt @@ -17,23 +17,22 @@ package im.vector.riotx.features.home.room.list import im.vector.matrix.android.api.session.Session -import im.vector.riotx.features.home.HomeRoomListObservableStore -import im.vector.riotx.features.share.ShareRoomListObservableStore +import im.vector.riotx.features.home.HomeRoomListDataSource +import im.vector.riotx.features.home.RoomListDisplayMode +import im.vector.riotx.features.share.ShareRoomListDataSource import javax.inject.Inject import javax.inject.Provider class RoomListViewModelFactory @Inject constructor(private val session: Provider, - private val homeRoomListObservableStore: Provider, - private val shareRoomListObservableStore: Provider, - private val alphabeticalRoomComparator: Provider, - private val chronologicalRoomComparator: Provider) : RoomListViewModel.Factory { + private val homeRoomListDataSource: Provider, + private val shareRoomListDataSource: Provider) + : RoomListViewModel.Factory { override fun create(initialState: RoomListViewState): RoomListViewModel { return RoomListViewModel( initialState, session.get(), - if (initialState.displayMode == RoomListFragment.DisplayMode.SHARE) shareRoomListObservableStore.get() else homeRoomListObservableStore.get(), - alphabeticalRoomComparator.get(), - chronologicalRoomComparator.get()) + if (initialState.displayMode == RoomListDisplayMode.SHARE) shareRoomListDataSource.get() else homeRoomListDataSource.get() + ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt index 505554a8fb..b41b4b9eeb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt @@ -23,9 +23,10 @@ import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.R +import im.vector.riotx.features.home.RoomListDisplayMode data class RoomListViewState( - val displayMode: RoomListFragment.DisplayMode, + val displayMode: RoomListDisplayMode, val asyncRooms: Async> = Uninitialized, val roomFilter: String = "", val asyncFilteredRooms: Async = Uninitialized, 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 0288590833..74dab6563f 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 @@ -21,15 +21,19 @@ import com.airbnb.epoxy.EpoxyController import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.R +import im.vector.riotx.core.epoxy.helpFooterItem import im.vector.riotx.core.epoxy.noResultItem import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.RoomListDisplayMode +import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.features.home.room.filtered.FilteredRoomFooterItem import im.vector.riotx.features.home.room.filtered.filteredRoomFooterItem import javax.inject.Inject class RoomSummaryController @Inject constructor(private val stringProvider: StringProvider, private val roomSummaryItemFactory: RoomSummaryItemFactory, - private val roomListNameFilter: RoomListNameFilter + private val roomListNameFilter: RoomListNameFilter, + private val userPreferencesProvider: UserPreferencesProvider ) : EpoxyController() { var listener: Listener? = null @@ -47,14 +51,20 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri requestModelBuild() } + fun onRoomLongClicked() { + userPreferencesProvider.neverShowLongClickOnRoomHelpAgain() + requestModelBuild() + } + override fun buildModels() { val nonNullViewState = viewState ?: return when (nonNullViewState.displayMode) { - RoomListFragment.DisplayMode.FILTERED, - RoomListFragment.DisplayMode.SHARE -> { + RoomListDisplayMode.FILTERED, + RoomListDisplayMode.SHARE -> { buildFilteredRooms(nonNullViewState) } else -> { + var showHelp = false val roomSummaries = nonNullViewState.asyncFilteredRooms() roomSummaries?.forEach { (category, summaries) -> if (summaries.isEmpty()) { @@ -70,9 +80,14 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri nonNullViewState.joiningErrorRoomsIds, nonNullViewState.rejectingRoomsIds, nonNullViewState.rejectingErrorRoomsIds) + showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp() } } } + + if (showHelp) { + buildLongClickHelp() + } } } } @@ -92,11 +107,18 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri viewState.rejectingErrorRoomsIds) when { - viewState.displayMode == RoomListFragment.DisplayMode.FILTERED -> addFilterFooter(viewState) + viewState.displayMode == RoomListDisplayMode.FILTERED -> addFilterFooter(viewState) filteredSummaries.isEmpty() -> addEmptyFooter() } } + private fun buildLongClickHelp() { + helpFooterItem { + id("long_click_help") + text(stringProvider.getString(R.string.help_long_click_on_room_for_more_options)) + } + } + private fun addFilterFooter(viewState: RoomListViewState) { filteredRoomFooterItem { id("filter_footer") @@ -154,7 +176,8 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri interface Listener : FilteredRoomFooterItem.FilteredRoomFooterItemListener { fun onToggleRoomCategory(roomCategory: RoomCategory) - fun onRoomSelected(room: RoomSummary) + fun onRoomClicked(room: RoomSummary) + fun onRoomLongClicked(room: RoomSummary): Boolean fun onRejectRoomInvitation(room: RoomSummary) fun onAcceptRoomInvitation(room: RoomSummary) } 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 812ad463f5..fe208a3085 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 @@ -41,11 +41,13 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { @EpoxyAttribute var hasUnreadMessage: Boolean = false @EpoxyAttribute var hasDraft: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false - @EpoxyAttribute var listener: (() -> Unit)? = null + @EpoxyAttribute var itemLongClickListener: View.OnLongClickListener? = null + @EpoxyAttribute var itemClickListener: View.OnClickListener? = null override fun bind(holder: Holder) { super.bind(holder) - holder.rootView.setOnClickListener { listener?.invoke() } + holder.rootView.setOnClickListener(itemClickListener) + holder.rootView.setOnLongClickListener(itemLongClickListener) holder.titleView.text = roomName holder.lastEventTimeView.text = lastEventTime holder.lastEventView.text = lastFormattedEvent 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 0ad3b10159..85652c4139 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 @@ -16,11 +16,11 @@ package im.vector.riotx.features.home.room.list +import android.view.View 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 import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.riotx.R import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -28,9 +28,9 @@ import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.DateProvider import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.features.home.room.detail.timeline.helper.senderName import me.gujun.android.span.span import javax.inject.Inject @@ -79,7 +79,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte .rejectListener { listener?.onRejectRoomInvitation(roomSummary) } .roomName(roomSummary.displayName) .avatarUrl(roomSummary.avatarUrl) - .listener { listener?.onRoomSelected(roomSummary) } + .listener { listener?.onRoomClicked(roomSummary) } } private fun createRoomItem(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { @@ -96,11 +96,11 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte latestFormattedEvent = if (latestEvent.root.isEncrypted() && latestEvent.root.mxDecryptionResult == null) { stringProvider.getString(R.string.encrypted_message) - } else if (latestEvent.root.getClearType() == EventType.MESSAGE) { - val senderName = latestEvent.senderName() ?: latestEvent.root.senderId - val content = latestEvent.root.getClearContent()?.toModel() + } else if (latestEvent.root.getClearType() == EventType.MESSAGE || latestEvent.root.getClearType() == EventType.STICKER) { + val senderName = latestEvent.getDisambiguatedDisplayName() + val content = latestEvent.getLastMessageContent() val message = content?.body ?: "" - if (roomSummary.isDirect.not() && senderName != null) { + if (roomSummary.isDirect.not()) { span { text = senderName textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary) @@ -134,6 +134,13 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte .unreadNotificationCount(unreadCount) .hasUnreadMessage(roomSummary.hasUnreadMessages) .hasDraft(roomSummary.userDrafts.isNotEmpty()) - .listener { listener?.onRoomSelected(roomSummary) } + .itemLongClickListener { _ -> + listener?.onRoomLongClicked(roomSummary) ?: false + } + .itemClickListener( + DebouncedClickListener(View.OnClickListener { _ -> + listener?.onRoomClicked(roomSummary) + }) + ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt new file mode 100644 index 0000000000..3a85cf26fa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.list.actions + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import butterknife.ButterKnife +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.navigation.Navigator +import kotlinx.android.parcel.Parcelize +import javax.inject.Inject + +@Parcelize +data class RoomListActionsArgs( + val roomId: String +) : Parcelable + +/** + * Bottom sheet fragment that shows room information with list of contextual actions + */ +class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomListQuickActionsEpoxyController.Listener { + + private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel + @Inject lateinit var roomListActionsViewModelFactory: RoomListQuickActionsViewModel.Factory + @Inject lateinit var roomListActionsEpoxyController: RoomListQuickActionsEpoxyController + @Inject lateinit var navigator: Navigator + + private val viewModel: RoomListQuickActionsViewModel by fragmentViewModel(RoomListQuickActionsViewModel::class) + + @BindView(R.id.bottomSheetRecyclerView) + lateinit var recyclerView: RecyclerView + + override val showExpanded = true + + override fun injectWith(screenComponent: ScreenComponent) { + screenComponent.inject(this) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.bottom_sheet_generic_list, container, false) + ButterKnife.bind(this, view) + return view + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) + recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + recyclerView.adapter = roomListActionsEpoxyController.adapter + // Disable item animation + recyclerView.itemAnimator = null + roomListActionsEpoxyController.listener = this + } + + override fun invalidate() = withState(viewModel) { + roomListActionsEpoxyController.setData(it) + super.invalidate() + } + + override fun didSelectMenuAction(quickAction: RoomListQuickActionsSharedAction) { + sharedActionViewModel.post(quickAction) + dismiss() + } + + companion object { + fun newInstance(roomId: String): RoomListQuickActionsBottomSheet { + return RoomListQuickActionsBottomSheet().apply { + setArguments(RoomListActionsArgs(roomId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt new file mode 100644 index 0000000000..2e17464cc6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.home.room.list.actions + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.session.room.notification.RoomNotificationState +import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemAction +import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemRoomPreview +import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetItemSeparator +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +/** + * Epoxy controller for room list actions + */ +class RoomListQuickActionsEpoxyController @Inject constructor(private val avatarRenderer: AvatarRenderer) + : TypedEpoxyController() { + + var listener: Listener? = null + + override fun buildModels(state: RoomListQuickActionsState) { + val roomSummary = state.roomSummary() ?: return + + // Preview + bottomSheetItemRoomPreview { + id("preview") + avatarRenderer(avatarRenderer) + roomName(roomSummary.displayName) + avatarUrl(roomSummary.avatarUrl) + roomId(roomSummary.roomId) + settingsClickListener(View.OnClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Settings(roomSummary.roomId)) }) + } + + // Notifications + bottomSheetItemSeparator { + id("notifications_separator") + } + + val selectedRoomState = state.roomNotificationState() + RoomListQuickActionsSharedAction.NotificationsAllNoisy(roomSummary.roomId).toBottomSheetItem(0, selectedRoomState) + RoomListQuickActionsSharedAction.NotificationsAll(roomSummary.roomId).toBottomSheetItem(1, selectedRoomState) + RoomListQuickActionsSharedAction.NotificationsMentionsOnly(roomSummary.roomId).toBottomSheetItem(2, selectedRoomState) + RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState) + + // Leave + bottomSheetItemSeparator { + id("leave_separator") + } + RoomListQuickActionsSharedAction.Leave(roomSummary.roomId).toBottomSheetItem(5) + } + + private fun RoomListQuickActionsSharedAction.toBottomSheetItem(index: Int, roomNotificationState: RoomNotificationState? = null) { + val selected = when (this) { + is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> roomNotificationState == RoomNotificationState.ALL_MESSAGES_NOISY + is RoomListQuickActionsSharedAction.NotificationsAll -> roomNotificationState == RoomNotificationState.ALL_MESSAGES + is RoomListQuickActionsSharedAction.NotificationsMentionsOnly -> roomNotificationState == RoomNotificationState.MENTIONS_ONLY + is RoomListQuickActionsSharedAction.NotificationsMute -> roomNotificationState == RoomNotificationState.MUTE + is RoomListQuickActionsSharedAction.Settings, + is RoomListQuickActionsSharedAction.Leave -> false + } + return bottomSheetItemAction { + id("action_$index") + selected(selected) + iconRes(iconResId) + textRes(titleRes) + destructive(this@toBottomSheetItem.destructive) + listener(View.OnClickListener { listener?.didSelectMenuAction(this@toBottomSheetItem) }) + } + } + + interface Listener { + fun didSelectMenuAction(quickAction: RoomListQuickActionsSharedAction) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt new file mode 100644 index 0000000000..ca006ddd7d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.list.actions + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorSharedAction + +sealed class RoomListQuickActionsSharedAction( + @StringRes val titleRes: Int, + @DrawableRes val iconResId: Int, + val destructive: Boolean = false) + : VectorSharedAction { + + data class NotificationsAllNoisy(val roomId: String) : RoomListQuickActionsSharedAction( + R.string.room_list_quick_actions_notifications_all_noisy, + R.drawable.ic_room_actions_notifications_all_noisy + ) + + data class NotificationsAll(val roomId: String) : RoomListQuickActionsSharedAction( + R.string.room_list_quick_actions_notifications_all, + R.drawable.ic_room_actions_notifications_all + ) + + data class NotificationsMentionsOnly(val roomId: String) : RoomListQuickActionsSharedAction( + R.string.room_list_quick_actions_notifications_mentions, + R.drawable.ic_room_actions_notifications_mentions + ) + + data class NotificationsMute(val roomId: String) : RoomListQuickActionsSharedAction( + R.string.room_list_quick_actions_notifications_mute, + R.drawable.ic_room_actions_notifications_mutes + ) + + data class Settings(val roomId: String) : RoomListQuickActionsSharedAction( + R.string.room_list_quick_actions_settings, + R.drawable.ic_room_actions_settings + ) + + data class Leave(val roomId: String) : RoomListQuickActionsSharedAction( + R.string.room_list_quick_actions_leave, + R.drawable.ic_room_actions_leave, + true + ) +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsSharedActionViewModel.kt new file mode 100644 index 0000000000..2f7e8354bf --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsSharedActionViewModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package im.vector.riotx.features.home.room.list.actions + +import im.vector.riotx.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +/** + * Activity shared view model to handle room list quick actions + */ +class RoomListQuickActionsSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsState.kt new file mode 100644 index 0000000000..a943db1804 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsState.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.list.actions + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.notification.RoomNotificationState + +data class RoomListQuickActionsState( + val roomId: String, + val roomSummary: Async = Uninitialized, + val roomNotificationState: Async = Uninitialized +) : MvRxState { + + constructor(args: RoomListActionsArgs) : this(roomId = args.roomId) +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsViewModel.kt new file mode 100644 index 0000000000..7f7a1f41c4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsViewModel.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.home.room.list.actions + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.unwrap +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel + +class RoomListQuickActionsViewModel @AssistedInject constructor(@Assisted initialState: RoomListQuickActionsState, + session: Session +) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomListQuickActionsState): RoomListQuickActionsViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: RoomListQuickActionsState): RoomListQuickActionsViewModel? { + val fragment: RoomListQuickActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.roomListActionsViewModelFactory.create(state) + } + } + + private val room = session.getRoom(initialState.roomId)!! + + init { + observeRoomSummary() + observeNotificationState() + } + + private fun observeNotificationState() { + room + .rx() + .liveNotificationState() + .execute { + copy(roomNotificationState = it) + } + } + + private fun observeRoomSummary() { + room + .rx() + .liveRoomSummary() + .unwrap() + .execute { + copy(roomSummary = it) + } + } + + override fun handle(action: EmptyAction) { + // No op + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt b/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt new file mode 100644 index 0000000000..ed8db94fc3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/CodeVisitor.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.html + +import org.commonmark.node.AbstractVisitor +import org.commonmark.node.Code +import org.commonmark.node.FencedCodeBlock +import org.commonmark.node.IndentedCodeBlock + +/** + * This class is in charge of visiting nodes and tells if we have some code nodes (inline or block). + */ +class CodeVisitor : AbstractVisitor() { + + var codeKind: Kind = Kind.NONE + private set + + override fun visit(fencedCodeBlock: FencedCodeBlock?) { + if (codeKind == Kind.NONE) { + codeKind = Kind.BLOCK + } + } + + override fun visit(indentedCodeBlock: IndentedCodeBlock?) { + if (codeKind == Kind.NONE) { + codeKind = Kind.BLOCK + } + } + + override fun visit(code: Code?) { + if (codeKind == Kind.NONE) { + codeKind = Kind.INLINE + } + } + + enum class Kind { + NONE, + INLINE, + BLOCK + } +} 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 06af8ebca5..7775de830f 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 @@ -17,171 +17,49 @@ package im.vector.riotx.features.html import android.content.Context -import android.text.style.URLSpan -import im.vector.matrix.android.api.permalinks.PermalinkData -import im.vector.matrix.android.api.permalinks.PermalinkParser import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideApp -import im.vector.riotx.core.glide.GlideRequests +import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.features.home.AvatarRenderer -import org.commonmark.node.BlockQuote -import org.commonmark.node.HtmlBlock -import org.commonmark.node.HtmlInline +import io.noties.markwon.Markwon +import io.noties.markwon.html.HtmlPlugin +import io.noties.markwon.html.TagHandlerNoOp import org.commonmark.node.Node -import ru.noties.markwon.* -import ru.noties.markwon.html.HtmlTag -import ru.noties.markwon.html.MarkwonHtmlParserImpl -import ru.noties.markwon.html.MarkwonHtmlRenderer -import ru.noties.markwon.html.TagHandler -import ru.noties.markwon.html.tag.* -import java.util.Arrays.asList import javax.inject.Inject import javax.inject.Singleton @Singleton class EventHtmlRenderer @Inject constructor(context: Context, - avatarRenderer: AvatarRenderer, - sessionHolder: ActiveSessionHolder) { + htmlConfigure: MatrixHtmlPluginConfigure) { + private val markwon = Markwon.builder(context) - .usePlugin(MatrixPlugin.create(GlideApp.with(context), context, avatarRenderer, sessionHolder)) + .usePlugin(HtmlPlugin.create(htmlConfigure)) .build() + fun parse(text: String): Node { + return markwon.parse(text) + } + fun render(text: String): CharSequence { return markwon.toMarkdown(text) } - fun render(node: Node) : CharSequence { + fun render(node: Node): CharSequence { return markwon.render(node) } } -private class MatrixPlugin private constructor(private val glideRequests: GlideRequests, - private val context: Context, - private val avatarRenderer: AvatarRenderer, - private val session: ActiveSessionHolder) : AbstractMarkwonPlugin() { +class MatrixHtmlPluginConfigure @Inject constructor(private val context: Context, + private val colorProvider: ColorProvider, + private val avatarRenderer: AvatarRenderer, + private val session: ActiveSessionHolder) : HtmlPlugin.HtmlConfigure { - override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { - builder.htmlParser(MarkwonHtmlParserImpl.create()) - } - - override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) { - builder - .setHandler( - "img", - ImageHandler.create()) - .setHandler( - "a", - MxLinkHandler(glideRequests, context, avatarRenderer, session)) - .setHandler( - "blockquote", - BlockquoteHandler()) - .setHandler( - "font", - FontTagHandler()) - .setHandler( - "sub", - SubScriptHandler()) - .setHandler( - "sup", - SuperScriptHandler()) - .setHandler( - asList("b", "strong"), - StrongEmphasisHandler()) - .setHandler( - asList("s", "del"), - StrikeHandler()) - .setHandler( - asList("u", "ins"), - UnderlineHandler()) - .setHandler( - asList("ul", "ol"), - ListHandler()) - .setHandler( - asList("i", "em", "cite", "dfn"), - EmphasisHandler()) - .setHandler( - asList("h1", "h2", "h3", "h4", "h5", "h6"), - HeadingHandler()) - .setHandler("mx-reply", - MxReplyTagHandler()) - } - - override fun afterRender(node: Node, visitor: MarkwonVisitor) { - val configuration = visitor.configuration() - configuration.htmlRenderer().render(visitor, configuration.htmlParser()) - } - - override fun configureVisitor(builder: MarkwonVisitor.Builder) { - builder - .on(HtmlBlock::class.java) { visitor, htmlBlock -> visitHtml(visitor, htmlBlock.literal) } - .on(HtmlInline::class.java) { visitor, htmlInline -> visitHtml(visitor, htmlInline.literal) } - } - - private fun visitHtml(visitor: MarkwonVisitor, html: String?) { - if (html != null) { - visitor.configuration().htmlParser().processFragment(visitor.builder(), html) - } - } - - companion object { - - fun create(glideRequests: GlideRequests, context: Context, avatarRenderer: AvatarRenderer, session: ActiveSessionHolder): MatrixPlugin { - return MatrixPlugin(glideRequests, context, avatarRenderer, session) - } - } -} - -private class MxLinkHandler(private val glideRequests: GlideRequests, - private val context: Context, - private val avatarRenderer: AvatarRenderer, - private val sessionHolder: ActiveSessionHolder) : TagHandler() { - - private val linkHandler = LinkHandler() - - override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { - val link = tag.attributes()["href"] - if (link != null) { - val permalinkData = PermalinkParser.parse(link) - when (permalinkData) { - is PermalinkData.UserLink -> { - val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId) - val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user) - SpannableBuilder.setSpans( - visitor.builder(), - span, - tag.start(), - tag.end() - ) - // also add clickable span - SpannableBuilder.setSpans( - visitor.builder(), - URLSpan(link), - tag.start(), - tag.end() - ) - } - else -> linkHandler.handle(visitor, renderer, tag) - } - } else { - linkHandler.handle(visitor, renderer, tag) - } - } -} - -private class MxReplyTagHandler : TagHandler() { - - override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { - val configuration = visitor.configuration() - val factory = configuration.spansFactory().get(BlockQuote::class.java) - if (factory != null) { - SpannableBuilder.setSpans( - visitor.builder(), - factory.getSpans(configuration, visitor.renderProps()), - tag.start(), - tag.end() - ) - val replyText = visitor.builder().removeFromEnd(tag.end()) - visitor.builder().append("\n\n").append(replyText) - } + override fun configureHtml(plugin: HtmlPlugin) { + plugin + .addHandler(TagHandlerNoOp.create("a")) + .addHandler(FontTagHandler()) + .addHandler(MxLinkTagHandler(GlideApp.with(context), context, avatarRenderer, session)) + .addHandler(MxReplyTagHandler()) + .addHandler(SpanHandler(colorProvider)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt index f4fa1737c9..e5733dd849 100644 --- a/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/html/FontTagHandler.kt @@ -17,15 +17,18 @@ package im.vector.riotx.features.html import android.graphics.Color import android.text.style.ForegroundColorSpan -import ru.noties.markwon.MarkwonConfiguration -import ru.noties.markwon.RenderProps -import ru.noties.markwon.html.HtmlTag -import ru.noties.markwon.html.tag.SimpleTagHandler +import io.noties.markwon.MarkwonConfiguration +import io.noties.markwon.RenderProps +import io.noties.markwon.html.HtmlTag +import io.noties.markwon.html.tag.SimpleTagHandler /** * custom to matrix for IRC-style font coloring */ class FontTagHandler : SimpleTagHandler() { + + override fun supportedTags() = listOf("font") + override fun getSpans(configuration: MarkwonConfiguration, renderProps: RenderProps, tag: HtmlTag): Any? { val colorString = tag.attributes()["color"]?.let { parseColor(it) } ?: Color.BLACK return ForegroundColorSpan(colorString) @@ -37,23 +40,23 @@ class FontTagHandler : SimpleTagHandler() { } catch (e: Exception) { // try other w3c colors? return when (color_name) { - "white" -> Color.WHITE - "yellow" -> Color.YELLOW + "white" -> Color.WHITE + "yellow" -> Color.YELLOW "fuchsia" -> Color.parseColor("#FF00FF") - "red" -> Color.RED - "silver" -> Color.parseColor("#C0C0C0") - "gray" -> Color.GRAY - "olive" -> Color.parseColor("#808000") - "purple" -> Color.parseColor("#800080") - "maroon" -> Color.parseColor("#800000") - "aqua" -> Color.parseColor("#00FFFF") - "lime" -> Color.parseColor("#00FF00") - "teal" -> Color.parseColor("#008080") - "green" -> Color.GREEN - "blue" -> Color.BLUE - "orange" -> Color.parseColor("#FFA500") - "navy" -> Color.parseColor("#000080") - else -> Color.BLACK + "red" -> Color.RED + "silver" -> Color.parseColor("#C0C0C0") + "gray" -> Color.GRAY + "olive" -> Color.parseColor("#808000") + "purple" -> Color.parseColor("#800080") + "maroon" -> Color.parseColor("#800000") + "aqua" -> Color.parseColor("#00FFFF") + "lime" -> Color.parseColor("#00FF00") + "teal" -> Color.parseColor("#008080") + "green" -> Color.GREEN + "blue" -> Color.BLUE + "orange" -> Color.parseColor("#FFA500") + "navy" -> Color.parseColor("#000080") + else -> Color.BLACK } } } diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt new file mode 100644 index 0000000000..fdcbb12cd7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/MxLinkTagHandler.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.html + +import android.content.Context +import android.text.style.URLSpan +import im.vector.matrix.android.api.permalinks.PermalinkData +import im.vector.matrix.android.api.permalinks.PermalinkParser +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.glide.GlideRequests +import im.vector.riotx.features.home.AvatarRenderer +import io.noties.markwon.MarkwonVisitor +import io.noties.markwon.SpannableBuilder +import io.noties.markwon.html.HtmlTag +import io.noties.markwon.html.MarkwonHtmlRenderer +import io.noties.markwon.html.tag.LinkHandler + +class MxLinkTagHandler(private val glideRequests: GlideRequests, + private val context: Context, + private val avatarRenderer: AvatarRenderer, + private val sessionHolder: ActiveSessionHolder) : LinkHandler() { + + override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { + val link = tag.attributes()["href"] + if (link != null) { + val permalinkData = PermalinkParser.parse(link) + when (permalinkData) { + is PermalinkData.UserLink -> { + val user = sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId) + val span = PillImageSpan(glideRequests, avatarRenderer, context, permalinkData.userId, user) + SpannableBuilder.setSpans( + visitor.builder(), + span, + tag.start(), + tag.end() + ) + // also add clickable span + SpannableBuilder.setSpans( + visitor.builder(), + URLSpan(link), + tag.start(), + tag.end() + ) + } + else -> super.handle(visitor, renderer, tag) + } + } else { + super.handle(visitor, renderer, tag) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/html/MxReplyTagHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/MxReplyTagHandler.kt new file mode 100644 index 0000000000..f999e253c7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/MxReplyTagHandler.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.html + +import io.noties.markwon.MarkwonVisitor +import io.noties.markwon.SpannableBuilder +import io.noties.markwon.html.HtmlTag +import io.noties.markwon.html.MarkwonHtmlRenderer +import io.noties.markwon.html.TagHandler +import org.commonmark.node.BlockQuote + +class MxReplyTagHandler : TagHandler() { + + override fun supportedTags() = listOf("mx-reply") + + override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { + val configuration = visitor.configuration() + val factory = configuration.spansFactory().get(BlockQuote::class.java) + if (factory != null) { + SpannableBuilder.setSpans( + visitor.builder(), + factory.getSpans(configuration, visitor.renderProps()), + tag.start(), + tag.end() + ) + val replyText = visitor.builder().removeFromEnd(tag.end()) + visitor.builder().append("\n\n").append(replyText) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/html/SpanHandler.kt b/vector/src/main/java/im/vector/riotx/features/html/SpanHandler.kt new file mode 100644 index 0000000000..cb6e4dc325 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/SpanHandler.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.html + +import im.vector.riotx.core.resources.ColorProvider +import io.noties.markwon.MarkwonVisitor +import io.noties.markwon.SpannableBuilder +import io.noties.markwon.html.HtmlTag +import io.noties.markwon.html.MarkwonHtmlRenderer +import io.noties.markwon.html.TagHandler + +class SpanHandler(private val colorProvider: ColorProvider) : TagHandler() { + + override fun supportedTags() = listOf("span") + + override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { + val mxSpoiler = tag.attributes()["data-mx-spoiler"] + if (mxSpoiler != null) { + SpannableBuilder.setSpans( + visitor.builder(), + SpoilerSpan(colorProvider), + tag.start(), + tag.end() + ) + } else { + // default thing? + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/html/SpoilerSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/SpoilerSpan.kt new file mode 100644 index 0000000000..5ba464cba4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/html/SpoilerSpan.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.html + +import android.graphics.Color +import android.text.TextPaint +import android.text.style.ClickableSpan +import android.view.View +import im.vector.riotx.R +import im.vector.riotx.core.resources.ColorProvider + +class SpoilerSpan(private val colorProvider: ColorProvider) : ClickableSpan() { + + override fun onClick(widget: View) { + isHidden = !isHidden + widget.invalidate() + } + + private var isHidden = true + + override fun updateDrawState(tp: TextPaint) { + if (isHidden) { + tp.bgColor = colorProvider.getColorFromAttribute(R.attr.vctr_spoiler_background_color) + tp.color = Color.TRANSPARENT + } else { + tp.bgColor = colorProvider.getColorFromAttribute(R.attr.vctr_markdown_block_background_color) + tp.color = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt similarity index 85% rename from vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt rename to vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 2c44737058..bb42bc8e0c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -17,12 +17,12 @@ package im.vector.riotx.features.login import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.riotx.core.platform.VectorViewModelAction -sealed class LoginActions { - - data class UpdateHomeServer(val homeServerUrl: String) : LoginActions() - data class Login(val login: String, val password: String) : LoginActions() - data class SsoLoginSuccess(val credentials: Credentials) : LoginActions() - data class NavigateTo(val target: LoginActivity.Navigation) : LoginActions() - data class InitWith(val loginConfig: LoginConfig) : LoginActions() +sealed class LoginAction : VectorViewModelAction { + data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() + data class Login(val login: String, val password: String) : LoginAction() + data class SsoLoginSuccess(val credentials: Credentials) : LoginAction() + data class NavigateTo(val target: LoginActivity.Navigation) : LoginAction() + data class InitWith(val loginConfig: LoginConfig) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 2cb28be998..abed22cb5e 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -51,18 +51,18 @@ class LoginActivity : VectorBaseActivity() { override fun initUiAndData() { if (isFirstCreation()) { - addFragment(LoginFragment(), R.id.simpleFragmentContainer) + addFragment(R.id.simpleFragmentContainer, LoginFragment::class.java) } // Get config extra val loginConfig = intent.getParcelableExtra(EXTRA_CONFIG) if (loginConfig != null && isFirstCreation()) { - loginViewModel.handle(LoginActions.InitWith(loginConfig)) + loginViewModel.handle(LoginAction.InitWith(loginConfig)) } loginViewModel.navigationLiveData.observeEvent(this) { when (it) { - is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(LoginSsoFallbackFragment(), R.id.simpleFragmentContainer) + is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(R.id.simpleFragmentContainer, LoginSsoFallbackFragment::class.java) is Navigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } } 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 3da21aa1df..456e4b2bb3 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 @@ -26,8 +26,6 @@ import com.airbnb.mvrx.* import com.jakewharton.rxbinding3.view.focusChanges import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.setTextWithColoredPart import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.VectorBaseFragment @@ -43,20 +41,14 @@ import javax.inject.Inject * What can be improved: * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect */ -class LoginFragment : VectorBaseFragment() { +class LoginFragment @Inject constructor() : VectorBaseFragment() { private val viewModel: LoginViewModel by activityViewModel() private var passwordShown = false - @Inject lateinit var errorFormatter: ErrorFormatter - override fun getLayoutResId() = R.layout.fragment_login - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -67,14 +59,14 @@ class LoginFragment : VectorBaseFragment() { homeServerField.focusChanges() .subscribe { if (!it) { - viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString())) + viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) } } - .disposeOnDestroy() + .disposeOnDestroyView() homeServerField.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { - viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString())) + viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) return@setOnEditorActionListener true } return@setOnEditorActionListener false @@ -86,7 +78,7 @@ class LoginFragment : VectorBaseFragment() { } else { homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext())) } - viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString())) + viewModel.handle(LoginAction.UpdateHomeServer(homeServerField.text.toString())) } private fun setupNotice() { @@ -101,7 +93,7 @@ class LoginFragment : VectorBaseFragment() { val login = loginField.text?.trim().toString() val password = passwordField.text?.trim().toString() - viewModel.handle(LoginActions.Login(login, password)) + viewModel.handle(LoginAction.Login(login, password)) } private fun setupAuthButton() { @@ -115,14 +107,14 @@ class LoginFragment : VectorBaseFragment() { } ) .subscribeBy { authenticateButton.isEnabled = it } - .disposeOnDestroy() + .disposeOnDestroyView() authenticateButton.setOnClickListener { authenticate() } authenticateButtonSso.setOnClickListener { openSso() } } private fun openSso() { - viewModel.handle(LoginActions.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback)) + viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback)) } private fun setupPasswordReveal() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt index cac981db84..38deccccaf 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -35,17 +35,17 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_login_sso_fallback.* import timber.log.Timber import java.net.URLDecoder +import javax.inject.Inject /** * Only login is supported for the moment */ -class LoginSsoFallbackFragment : VectorBaseFragment(), OnBackPressed { +class LoginSsoFallbackFragment @Inject constructor() : VectorBaseFragment(), OnBackPressed { private val viewModel: LoginViewModel by activityViewModel() @@ -62,10 +62,6 @@ class LoginSsoFallbackFragment : VectorBaseFragment(), OnBackPressed { override fun getLayoutResId() = R.layout.fragment_login_sso_fallback - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -147,7 +143,7 @@ class LoginSsoFallbackFragment : VectorBaseFragment(), OnBackPressed { super.onReceivedError(view, errorCode, description, failingUrl) // on error case, close this fragment - viewModel.handle(LoginActions.NavigateTo(LoginActivity.Navigation.GoBack)) + viewModel.handle(LoginAction.NavigateTo(LoginActivity.Navigation.GoBack)) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { @@ -257,7 +253,7 @@ class LoginSsoFallbackFragment : VectorBaseFragment(), OnBackPressed { refreshToken = null ) - viewModel.handle(LoginActions.SsoLoginSuccess(safeCredentials)) + viewModel.handle(LoginAction.SsoLoginSuccess(safeCredentials)) } } } catch (e: Exception) { @@ -282,7 +278,7 @@ class LoginSsoFallbackFragment : VectorBaseFragment(), OnBackPressed { refreshToken = null ) - viewModel.handle(LoginActions.SsoLoginSuccess(credentials)) + viewModel.handle(LoginAction.SsoLoginSuccess(credentials)) } } } 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 0a324df6b9..a0a7258e2a 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 @@ -42,7 +42,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private val activeSessionHolder: ActiveSessionHolder, private val pushRuleTriggerListener: PushRuleTriggerListener, private val sessionListener: SessionListener) - : VectorViewModel(initialState) { + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -67,21 +67,21 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private var homeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentTask: Cancelable? = null - fun handle(action: LoginActions) { + override fun handle(action: LoginAction) { when (action) { - is LoginActions.InitWith -> handleInitWith(action) - is LoginActions.UpdateHomeServer -> handleUpdateHomeserver(action) - is LoginActions.Login -> handleLogin(action) - is LoginActions.SsoLoginSuccess -> handleSsoLoginSuccess(action) - is LoginActions.NavigateTo -> handleNavigation(action) + is LoginAction.InitWith -> handleInitWith(action) + is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) + is LoginAction.Login -> handleLogin(action) + is LoginAction.SsoLoginSuccess -> handleSsoLoginSuccess(action) + is LoginAction.NavigateTo -> handleNavigation(action) } } - private fun handleInitWith(action: LoginActions.InitWith) { + private fun handleInitWith(action: LoginAction.InitWith) { loginConfig = action.loginConfig } - private fun handleLogin(action: LoginActions.Login) { + private fun handleLogin(action: LoginAction.Login) { val homeServerConnectionConfigFinal = homeServerConnectionConfig if (homeServerConnectionConfigFinal == null) { @@ -116,7 +116,6 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun onSessionCreated(session: Session) { activeSessionHolder.setActiveSession(session) session.configureAndStart(pushRuleTriggerListener, sessionListener) - setState { copy( asyncLoginAction = Success(Unit) @@ -124,20 +123,26 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } - private fun handleSsoLoginSuccess(action: LoginActions.SsoLoginSuccess) { + private fun handleSsoLoginSuccess(action: LoginAction.SsoLoginSuccess) { val homeServerConnectionConfigFinal = homeServerConnectionConfig if (homeServerConnectionConfigFinal == null) { // Should not happen Timber.w("homeServerConnectionConfig is null") } else { - val session = authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfigFinal) + authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfigFinal, object : MatrixCallback { + override fun onSuccess(data: Session) { + onSessionCreated(data) + } - onSessionCreated(session) + override fun onFailure(failure: Throwable) = setState { + copy(asyncLoginAction = Fail(failure)) + } + }) } } - private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) = withState { state -> + private fun handleUpdateHomeserver(action: LoginAction.UpdateHomeServer) = withState { state -> var newConfig: HomeServerConnectionConfig? = null Try { @@ -149,7 +154,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi // 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 + && state.asyncHomeServerLoginFlowRequest is Success) return@withState currentTask?.cancel() homeServerConnectionConfig = newConfig @@ -197,7 +202,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi } } - private fun handleNavigation(action: LoginActions.NavigateTo) { + private fun handleNavigation(action: LoginAction.NavigateTo) { _navigationLiveData.postValue(LiveEvent(action.target)) } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index a3f9c009ed..685fa04fef 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -112,4 +112,8 @@ class DefaultNavigator @Inject constructor() : Navigator { override fun openUserDetail(userId: String, context: Context) { Timber.v("Open user detail $userId") } + + override fun openRoomSettings(context: Context, roomId: String) { + Timber.v("Open room settings$roomId") + } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 4112dbbfc8..83c4f7ce20 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -50,4 +50,6 @@ interface Navigator { fun openGroupDetail(groupId: String, context: Context) fun openUserDetail(userId: String, context: Context) + + fun openRoomSettings(context: Context, roomId: String) } 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 06108e07fe..e38e7d548a 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 @@ -33,7 +33,7 @@ import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter import timber.log.Timber -import java.util.* +import java.util.UUID import javax.inject.Inject /** @@ -94,7 +94,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St 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 senderDisplayName = event.getDisambiguatedDisplayName() val notifiableEvent = NotifiableMessageEvent( eventId = event.root.eventId!!, @@ -128,7 +128,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St 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 senderDisplayName = event.getDisambiguatedDisplayName() val notifiableEvent = NotifiableMessageEvent( eventId = event.root.eventId!!, 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 e26395641d..63cd1c5ce6 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 @@ -27,7 +27,7 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.vectorComponent import timber.log.Timber -import java.util.* +import java.util.UUID import javax.inject.Inject /** 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 1a5385663b..7d8e43d0be 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 @@ -45,9 +45,9 @@ import im.vector.riotx.features.home.room.detail.RoomDetailActivity 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 +import kotlin.random.Random /** * Util class for creating notifications. @@ -299,7 +299,7 @@ class NotificationUtils @Inject constructor(private val context: Context, // use a generator for the private requestCode. // When using 0, the intent is not created/launched when the user taps on the notification. // - val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT) + val pendingIntent = stackBuilder.getPendingIntent(Random.nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT) builder.setContentIntent(pendingIntent) @@ -599,7 +599,7 @@ class NotificationUtils @Inject constructor(private val context: Context, 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") - return PendingIntent.getActivity(context, Random().nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT) + return PendingIntent.getActivity(context, Random.nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT) } /* diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt index 9a7707d063..b96542a8ce 100755 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt @@ -46,7 +46,7 @@ import org.json.JSONObject import timber.log.Timber import java.io.* import java.net.HttpURLConnection -import java.util.* +import java.util.Locale import java.util.zip.GZIPOutputStream import javax.inject.Inject import javax.inject.Singleton diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt index 0b9cb5798c..95053790c8 100644 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt @@ -24,7 +24,9 @@ import java.io.File import java.io.PrintWriter import java.io.StringWriter import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale +import java.util.TimeZone import java.util.logging.* import java.util.logging.Formatter import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt index 8aec8231db..5e705a70c2 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt @@ -16,32 +16,25 @@ package im.vector.riotx.features.reactions import android.os.Bundle -import androidx.lifecycle.ViewModelProviders +import android.view.View import androidx.recyclerview.widget.RecyclerView import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment +import javax.inject.Inject -class EmojiChooserFragment : VectorBaseFragment() { - - companion object { - fun newInstance() = EmojiChooserFragment() - } +class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() { override fun getLayoutResId() = R.layout.emoji_chooser_fragment private lateinit var viewModel: EmojiChooserViewModel - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - viewModel = activity?.run { - ViewModelProviders.of(this, viewModelFactory).get(EmojiChooserViewModel::class.java) - } ?: throw Exception("Invalid Activity") - viewModel.initWithContect(context!!) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) + viewModel.initWithContext(context!!) (view as? RecyclerView)?.let { it.adapter = viewModel.adapter it.adapter?.notifyDataSetChanged() } - -// val ds = EmojiDataSource(this.context!!) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt index 16aecd0906..bbde2ac54c 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt @@ -39,7 +39,7 @@ class EmojiChooserViewModel @Inject constructor() : ViewModel() { } } - fun initWithContect(context: Context) { + fun initWithContext(context: Context) { // TODO load async val emojiDataSource = EmojiDataSource(context) emojiSourceLiveData.value = emojiDataSource diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt index 0a4e05a4c8..85e4eecf21 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt @@ -25,15 +25,20 @@ import android.view.MenuInflater import android.view.MenuItem import android.widget.SearchView import androidx.appcompat.widget.Toolbar +import androidx.core.view.isVisible import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders +import com.airbnb.mvrx.viewModel import com.google.android.material.tabs.TabLayout +import com.jakewharton.rxbinding3.widget.queryTextChanges import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseActivity +import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.* +import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Inject /** @@ -41,9 +46,9 @@ import javax.inject.Inject * TODO: Loading indicator while getting emoji data source? * TODO: migrate to MvRx * TODO: Finish Refactor to vector base activity - * TODO: Move font request to app */ -class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvider.FontProviderListener { +class EmojiReactionPickerActivity : VectorBaseActivity(), + EmojiCompatFontProvider.FontProviderListener { private lateinit var tabLayout: TabLayout @@ -57,6 +62,8 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider + private val searchResultViewModel: EmojiSearchResultViewModel by viewModel() + private var tabLayoutSelectionListener = object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab) { } @@ -82,7 +89,7 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide tabLayout = findViewById(R.id.tabs) - viewModel = ViewModelProviders.of(this, viewModelFactory).get(EmojiChooserViewModel::class.java) + viewModel = viewModelProvider.get(EmojiChooserViewModel::class.java) viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID) @@ -121,10 +128,15 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide finish() } } + + emojiPickerWholeListFragmentContainer.isVisible = true + emojiPickerFilteredListFragmentContainer.isVisible = false + tabLayout.isVisible = true } override fun compatibilityFontUpdate(typeface: Typeface?) { EmojiDrawView.configureTextPaint(this, typeface) + searchResultViewModel.dataSource } override fun onDestroy() { @@ -137,11 +149,11 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide inflater.inflate(getMenuRes(), menu) val searchItem = menu.findItem(R.id.search) - (searchItem.actionView as? SearchView)?.let { + (searchItem.actionView as? SearchView)?.let { searchView -> searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { override fun onMenuItemActionExpand(p0: MenuItem?): Boolean { - it.isIconified = false - it.requestFocusFromTouch() + searchView.isIconified = false + searchView.requestFocusFromTouch() // we want to force the tool bar as visible even if hidden with scroll flags findViewById(R.id.toolbar)?.minimumHeight = getActionBarSize() return true @@ -150,12 +162,20 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean { // when back, clear all search findViewById(R.id.toolbar)?.minimumHeight = 0 - it.setQuery("", true) + searchView.setQuery("", true) return true } }) - } + searchView.queryTextChanges() + .throttleWithTimeout(600, TimeUnit.MILLISECONDS) + .doOnError { err -> Timber.e(err) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { query -> + onQueryText(query.toString()) + } + .disposeOnDestroy() + } return true } @@ -171,6 +191,19 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvide } } + private fun onQueryText(query: String) { + if (query.isEmpty()) { + tabLayout.isVisible = true + emojiPickerWholeListFragmentContainer.isVisible = true + emojiPickerFilteredListFragmentContainer.isVisible = false + } else { + tabLayout.isVisible = false + emojiPickerWholeListFragmentContainer.isVisible = false + emojiPickerFilteredListFragmentContainer.isVisible = true + searchResultViewModel.handle(EmojiSearchAction.UpdateQuery(query)) + } + } + companion object { const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID" diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchAction.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchAction.kt new file mode 100644 index 0000000000..badc6ea1e4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.reactions + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class EmojiSearchAction : VectorViewModelAction { + data class UpdateQuery(val queryString: String) : EmojiSearchAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultController.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultController.kt new file mode 100644 index 0000000000..3e8f1c9769 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultController.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.reactions + +import android.graphics.Typeface +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.ui.list.genericFooterItem +import javax.inject.Inject + +class EmojiSearchResultController @Inject constructor(val stringProvider: StringProvider, + private val fontProvider: EmojiCompatFontProvider) + : TypedEpoxyController() { + + var emojiTypeface: Typeface? = fontProvider.typeface + + private val fontProviderListener = object : EmojiCompatFontProvider.FontProviderListener { + override fun compatibilityFontUpdate(typeface: Typeface?) { + emojiTypeface = typeface + } + } + + init { + fontProvider.addListener(fontProviderListener) + } + + var listener: ReactionClickListener? = null + + override fun buildModels(data: EmojiSearchResultViewState?) { + val results = data?.results ?: return + + if (results.isEmpty()) { + if (data.query.isEmpty()) { + // display 'Type something to find' + genericFooterItem { + id("type.query.item") + text(stringProvider.getString(R.string.reaction_search_type_hint)) + } + } else { + // Display no search Results + genericFooterItem { + id("no.results.item") + text(stringProvider.getString(R.string.no_result_placeholder)) + } + } + } else { + // Build the search results + results.forEach { + emojiSearchResultItem { + id(it.name) + emojiItem(it) + emojiTypeFace(emojiTypeface) + currentQuery(data.query) + onClickListener(listener) + } + } + } + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + fontProvider.removeListener(fontProviderListener) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultFragment.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultFragment.kt new file mode 100644 index 0000000000..029f468b70 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultFragment.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.reactions + +import android.os.Bundle +import android.view.View +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.EpoxyRecyclerView +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.LiveEvent +import javax.inject.Inject + +class EmojiSearchResultFragment @Inject constructor( + private val epoxyController: EmojiSearchResultController +) : VectorBaseFragment() { + + override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy + + val viewModel: EmojiSearchResultViewModel by activityViewModel() + + var sharedViewModel: EmojiChooserViewModel? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedViewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) + + epoxyController.listener = object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + sharedViewModel?.selectedReaction = reaction + sharedViewModel?.navigateEvent?.value = LiveEvent(EmojiChooserViewModel.NAVIGATE_FINISH) + } + } + + val lmgr = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + val epoxyRecyclerView = view as? EpoxyRecyclerView ?: return + epoxyRecyclerView.layoutManager = lmgr + val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, lmgr.orientation) + epoxyRecyclerView.addItemDecoration(dividerItemDecoration) + epoxyRecyclerView.setController(epoxyController) + } + + override fun invalidate() = withState(viewModel) { state -> + epoxyController.setData(state) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultItem.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultItem.kt new file mode 100644 index 0000000000..1b117035d9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultItem.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.reactions + +import android.graphics.Typeface +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder + +@EpoxyModelClass(layout = R.layout.item_emoji_result) +abstract class EmojiSearchResultItem : EpoxyModelWithHolder() { + + @EpoxyAttribute + lateinit var emojiItem: EmojiDataSource.EmojiItem + + @EpoxyAttribute + var currentQuery: String? = null + + @EpoxyAttribute + var onClickListener: ReactionClickListener? = null + + @EpoxyAttribute + var emojiTypeFace: Typeface? = null + + override fun bind(holder: Holder) { + super.bind(holder) + // TODO use query string to highlight the matched query in name and keywords? + holder.emojiText.text = emojiItem.emojiString() + holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT + holder.emojiNameText.text = emojiItem.name + holder.emojiKeywordText.text = emojiItem.keywords?.joinToString(", ") + holder.view.setOnClickListener { + onClickListener?.onReactionSelected(emojiItem.emojiString()) + } + } + + class Holder : VectorEpoxyHolder() { + val emojiText by bind(R.id.item_emoji_tv) + val emojiNameText by bind(R.id.item_emoji_name) + val emojiKeywordText by bind(R.id.item_emoji_keyword) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt new file mode 100644 index 0000000000..8225fa7bd6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.reactions + +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import im.vector.riotx.core.platform.VectorViewModel + +data class EmojiSearchResultViewState( + val query: String = "", + val results: List = emptyList() +) : MvRxState + +class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState: EmojiSearchResultViewState) + : VectorViewModel(initialState) { + + override fun handle(action: EmojiSearchAction) { + when (action) { + is EmojiSearchAction.UpdateQuery -> updateQuery(action) + } + } + + private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { + setState { + copy( + query = action.queryString, + results = dataSource.rawData?.emojis?.toList() + ?.map { it.second } + ?.filter { + it.name.contains(action.queryString, true) + || action.queryString.split("\\s".toRegex()).fold(true, { prev, q -> + prev && (it.keywords?.any { it.contains(q, true) } ?: false) + }) + } ?: emptyList() + ) + } + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? { + // TODO get the data source from activity? share it with other fragment + return EmojiSearchResultViewModel(EmojiDataSource(viewModelContext.activity), state) + } + } +} 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 955713c0f8..b41c563256 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 @@ -19,7 +19,6 @@ package im.vector.riotx.features.roomdirectory import android.os.Bundle import android.view.MenuItem import android.view.View -import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.activityViewModel @@ -28,7 +27,6 @@ import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseFragment @@ -42,22 +40,18 @@ import javax.inject.Inject * What can be improved: * - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect */ -class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback { +class PublicRoomsFragment @Inject constructor( + private val publicRoomsController: PublicRoomsController, + private val errorFormatter: ErrorFormatter +) : VectorBaseFragment(), PublicRoomsController.Callback { private val viewModel: RoomDirectoryViewModel by activityViewModel() - private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel - - @Inject lateinit var publicRoomsController: PublicRoomsController - @Inject lateinit var errorFormatter: ErrorFormatter + private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel override fun getLayoutResId() = R.layout.fragment_public_rooms override fun getMenuRes() = R.menu.menu_room_directory - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -71,12 +65,12 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback publicRoomsFilter.queryTextChanges() .debounce(500, TimeUnit.MILLISECONDS) .subscribeBy { - viewModel.filterWith(it.toString()) + viewModel.handle(RoomDirectoryAction.FilterWith(it.toString())) } - .disposeOnDestroy() + .disposeOnDestroyView() publicRoomsCreateNewRoom.setOnClickListener { - navigationViewModel.goTo(RoomDirectoryActivity.Navigation.CreateRoom) + sharedActionViewModel.post(RoomDirectorySharedAction.CreateRoom) } viewModel.joinRoomErrorLiveData.observeEvent(this) { throwable -> @@ -88,7 +82,7 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_room_directory_change_protocol -> { - navigationViewModel.goTo(RoomDirectoryActivity.Navigation.ChangeProtocol) + sharedActionViewModel.post(RoomDirectorySharedAction.ChangeProtocol) true } else -> @@ -98,7 +92,7 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - navigationViewModel = ViewModelProviders.of(requireActivity()).get(RoomDirectoryNavigationViewModel::class.java) + sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) setupRecyclerView() } @@ -135,14 +129,14 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback override fun onPublicRoomJoin(publicRoom: PublicRoom) { Timber.v("PublicRoomJoinClicked: $publicRoom") - viewModel.joinRoom(publicRoom) + viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.roomId)) } override fun loadMore() { - viewModel.loadMore() + viewModel.handle(RoomDirectoryAction.LoadMore) } - var initialValueSet = false + private var initialValueSet = false override fun invalidate() = withState(viewModel) { state -> if (!initialValueSet) { diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt new file mode 100644 index 0000000000..8b32726370 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryAction.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomdirectory + +import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RoomDirectoryAction : VectorViewModelAction { + data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction() + data class FilterWith(val filter: String) : RoomDirectoryAction() + object LoadMore : RoomDirectoryAction() + data class JoinRoom(val roomId: String) : RoomDirectoryAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryActivity.kt index b2dd13e59a..e5ae0d4f54 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryActivity.kt @@ -19,14 +19,13 @@ package im.vector.riotx.features.roomdirectory import android.content.Context import android.content.Intent import android.os.Bundle -import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.viewModel import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack -import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.roomdirectory.createroom.CreateRoomAction import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomViewModel import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment @@ -34,19 +33,11 @@ import javax.inject.Inject class RoomDirectoryActivity : VectorBaseActivity() { - // Supported navigation actions for this Activity - sealed class Navigation { - object Back : Navigation() - object CreateRoom : Navigation() - object Close : Navigation() - object ChangeProtocol : Navigation() - } - @Inject lateinit var createRoomViewModelFactory: CreateRoomViewModel.Factory @Inject lateinit var roomDirectoryViewModelFactory: RoomDirectoryViewModel.Factory private val roomDirectoryViewModel: RoomDirectoryViewModel by viewModel() private val createRoomViewModel: CreateRoomViewModel by viewModel() - private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel + private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel override fun getLayoutRes() = R.layout.activity_simple @@ -56,30 +47,35 @@ class RoomDirectoryActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(RoomDirectoryNavigationViewModel::class.java) + sharedActionViewModel = viewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) if (isFirstCreation()) { - roomDirectoryViewModel.filterWith(intent?.getStringExtra(INITIAL_FILTER) ?: "") + roomDirectoryViewModel.handle(RoomDirectoryAction.FilterWith(intent?.getStringExtra(INITIAL_FILTER) ?: "")) } - navigationViewModel.navigateTo.observeEvent(this) { navigation -> - when (navigation) { - is Navigation.Back -> onBackPressed() - is Navigation.CreateRoom -> addFragmentToBackstack(CreateRoomFragment(), R.id.simpleFragmentContainer) - is Navigation.ChangeProtocol -> addFragmentToBackstack(RoomDirectoryPickerFragment(), R.id.simpleFragmentContainer) - is Navigation.Close -> finish() - } - } + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + is RoomDirectorySharedAction.Back -> onBackPressed() + is RoomDirectorySharedAction.CreateRoom -> + addFragmentToBackstack(R.id.simpleFragmentContainer, CreateRoomFragment::class.java) + is RoomDirectorySharedAction.ChangeProtocol -> + addFragmentToBackstack(R.id.simpleFragmentContainer, RoomDirectoryPickerFragment::class.java) + is RoomDirectorySharedAction.Close -> finish() + } + } + .disposeOnDestroy() roomDirectoryViewModel.selectSubscribe(this, PublicRoomsViewState::currentFilter) { currentFilter -> // Transmit the filter to the createRoomViewModel - createRoomViewModel.setName(currentFilter) + createRoomViewModel.handle(CreateRoomAction.SetName(currentFilter)) } } override fun initUiAndData() { if (isFirstCreation()) { - addFragment(PublicRoomsFragment(), R.id.simpleFragmentContainer) + addFragment(R.id.simpleFragmentContainer, PublicRoomsFragment::class.java) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectorySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectorySharedAction.kt new file mode 100644 index 0000000000..b257f82595 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectorySharedAction.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomdirectory + +import im.vector.riotx.core.platform.VectorSharedAction + +/** + * Supported navigation actions for [RoomDirectoryActivity] + */ +sealed class RoomDirectorySharedAction : VectorSharedAction { + object Back : RoomDirectorySharedAction() + object CreateRoom : RoomDirectorySharedAction() + object Close : RoomDirectorySharedAction() + object ChangeProtocol : RoomDirectorySharedAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryNavigationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectorySharedActionViewModel.kt similarity index 78% rename from vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryNavigationViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectorySharedActionViewModel.kt index c87da6bd50..12ae9c1cb5 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryNavigationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectorySharedActionViewModel.kt @@ -16,7 +16,7 @@ package im.vector.riotx.features.roomdirectory -import im.vector.riotx.core.mvrx.NavigationViewModel +import im.vector.riotx.core.platform.VectorSharedActionViewModel import javax.inject.Inject -class RoomDirectoryNavigationViewModel @Inject constructor(): NavigationViewModel() +class RoomDirectorySharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() 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 b0a47199d9..685e1aa282 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 @@ -25,7 +25,6 @@ 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 import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsFilter import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse @@ -40,7 +39,8 @@ import timber.log.Timber private const val PUBLIC_ROOMS_LIMIT = 20 class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: PublicRoomsViewState, - private val session: Session) : VectorViewModel(initialState) { + private val session: Session) + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -103,23 +103,32 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: .disposeOnClear() } - fun setRoomDirectoryData(roomDirectoryData: RoomDirectoryData) { - if (this.roomDirectoryData == roomDirectoryData) { + override fun handle(action: RoomDirectoryAction) { + when (action) { + is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action) + is RoomDirectoryAction.FilterWith -> filterWith(action) + RoomDirectoryAction.LoadMore -> loadMore() + is RoomDirectoryAction.JoinRoom -> joinRoom(action) + } + } + + private fun setRoomDirectoryData(action: RoomDirectoryAction.SetRoomDirectoryData) { + if (this.roomDirectoryData == action.roomDirectoryData) { return } - this.roomDirectoryData = roomDirectoryData + this.roomDirectoryData = action.roomDirectoryData reset("") load("") } - fun filterWith(filter: String) = withState { state -> - if (state.currentFilter != filter) { + private fun filterWith(action: RoomDirectoryAction.FilterWith) = withState { state -> + if (state.currentFilter != action.filter) { currentTask?.cancel() - reset(filter) - load(filter) + reset(action.filter) + load(action.filter) } } @@ -138,7 +147,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: } } - fun loadMore() = withState { state -> + private fun loadMore() = withState { state -> if (currentTask == null) { setState { copy( @@ -192,8 +201,8 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: }) } - fun joinRoom(publicRoom: PublicRoom) = withState { state -> - if (state.joiningRoomsIds.contains(publicRoom.roomId)) { + private fun joinRoom(action: RoomDirectoryAction.JoinRoom) = withState { state -> + if (state.joiningRoomsIds.contains(action.roomId)) { // Request already sent, should not happen Timber.w("Try to join an already joining room. Should not happen") return@withState @@ -201,11 +210,11 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: setState { copy( - joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { add(publicRoom.roomId) } + joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { add(action.roomId) } ) } - session.joinRoom(publicRoom.roomId, emptyList(), object : MatrixCallback { + session.joinRoom(action.roomId, emptyList(), object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined @@ -217,8 +226,8 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: setState { copy( - joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { remove(publicRoom.roomId) }, - joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { add(publicRoom.roomId) } + joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { remove(action.roomId) }, + joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { add(action.roomId) } ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomAction.kt new file mode 100644 index 0000000000..333834ca3c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomAction.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomdirectory.createroom + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class CreateRoomAction : VectorViewModelAction { + data class SetName(val name: String) : CreateRoomAction() + data class SetIsPublic(val isPublic: Boolean) : CreateRoomAction() + data class SetIsInRoomDirectory(val isInRoomDirectory: Boolean) : CreateRoomAction() + object Create : CreateRoomAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt index 9a3e903954..a83208c98a 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomActivity.kt @@ -20,16 +20,13 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.appcompat.widget.Toolbar -import androidx.lifecycle.ViewModelProviders import com.airbnb.mvrx.viewModel import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment -import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity -import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity -import im.vector.riotx.features.roomdirectory.RoomDirectoryNavigationViewModel +import im.vector.riotx.features.roomdirectory.RoomDirectorySharedAction +import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import javax.inject.Inject /** @@ -40,7 +37,7 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var createRoomViewModelFactory: CreateRoomViewModel.Factory private val createRoomViewModel: CreateRoomViewModel by viewModel() - private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel + private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel override fun getLayoutRes() = R.layout.activity_simple @@ -50,25 +47,23 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable { override fun initUiAndData() { if (isFirstCreation()) { - addFragment(CreateRoomFragment(), R.id.simpleFragmentContainer) - - createRoomViewModel.setName(intent?.getStringExtra(INITIAL_NAME) ?: "") + addFragment(R.id.simpleFragmentContainer, CreateRoomFragment::class.java) + createRoomViewModel.handle(CreateRoomAction.SetName(intent?.getStringExtra(INITIAL_NAME) ?: "")) } } - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - navigationViewModel = ViewModelProviders.of(this, viewModelFactory).get(RoomDirectoryNavigationViewModel::class.java) - navigationViewModel.navigateTo.observeEvent(this) { navigation -> - when (navigation) { - is RoomDirectoryActivity.Navigation.Back, - is RoomDirectoryActivity.Navigation.Close -> finish() - } - } + sharedActionViewModel = viewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + is RoomDirectorySharedAction.Back, + is RoomDirectorySharedAction.Close -> finish() + } + } + .disposeOnDestroy() } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt index acea19b49a..0dec14f50e 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -18,48 +18,42 @@ package im.vector.riotx.features.roomdirectory.createroom import android.os.Bundle import android.view.MenuItem -import androidx.lifecycle.ViewModelProviders +import android.view.View import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.mvrx.Success import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity -import im.vector.riotx.features.roomdirectory.RoomDirectoryNavigationViewModel +import im.vector.riotx.features.roomdirectory.RoomDirectorySharedAction +import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import kotlinx.android.synthetic.main.fragment_create_room.* import timber.log.Timber import javax.inject.Inject -class CreateRoomFragment : VectorBaseFragment(), CreateRoomController.Listener { +class CreateRoomFragment @Inject constructor(private val createRoomController: CreateRoomController) : VectorBaseFragment(), CreateRoomController.Listener { - private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel + private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel private val viewModel: CreateRoomViewModel by activityViewModel() - @Inject lateinit var createRoomController: CreateRoomController override fun getLayoutResId() = R.layout.fragment_create_room override fun getMenuRes() = R.menu.vector_room_creation - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) vectorBaseActivity.setSupportActionBar(createRoomToolbar) - navigationViewModel = ViewModelProviders.of(requireActivity()).get(RoomDirectoryNavigationViewModel::class.java) + sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) setupRecyclerView() createRoomClose.setOnClickListener { - navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Back) + sharedActionViewModel.post(RoomDirectorySharedAction.Back) } } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_create_room -> { - viewModel.doCreateRoom() + viewModel.handle(CreateRoomAction.Create) true } else -> @@ -77,20 +71,20 @@ class CreateRoomFragment : VectorBaseFragment(), CreateRoomController.Listener { } override fun onNameChange(newName: String) { - viewModel.setName(newName) + viewModel.handle(CreateRoomAction.SetName(newName)) } override fun setIsPublic(isPublic: Boolean) { - viewModel.setIsPublic(isPublic) + viewModel.handle(CreateRoomAction.SetIsPublic(isPublic)) } override fun setIsInRoomDirectory(isInRoomDirectory: Boolean) { - viewModel.setIsInRoomDirectory(isInRoomDirectory) + viewModel.handle(CreateRoomAction.SetIsInRoomDirectory(isInRoomDirectory)) } override fun retry() { Timber.v("Retry") - viewModel.doCreateRoom() + viewModel.handle(CreateRoomAction.Create) } override fun invalidate() = withState(viewModel) { state -> @@ -99,7 +93,7 @@ class CreateRoomFragment : VectorBaseFragment(), CreateRoomController.Listener { // Navigate to freshly created room navigator.openRoom(requireActivity(), async()) - navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Close) + sharedActionViewModel.post(RoomDirectorySharedAction.Close) } else { // Populate list with Epoxy createRoomController.setData(state) diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt index 28185e91c1..ff4aa332f4 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -30,7 +30,7 @@ import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateRoomViewState, private val session: Session -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -46,18 +46,27 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr return when (activity) { is CreateRoomActivity -> activity.createRoomViewModelFactory.create(state) is RoomDirectoryActivity -> activity.createRoomViewModelFactory.create(state) - else -> throw IllegalStateException("Wrong activity") + else -> error("Wrong activity") } } } - fun setName(newName: String) = setState { copy(roomName = newName) } + override fun handle(action: CreateRoomAction) { + when (action) { + is CreateRoomAction.SetName -> setName(action) + is CreateRoomAction.SetIsPublic -> setIsPublic(action) + is CreateRoomAction.SetIsInRoomDirectory -> setIsInRoomDirectory(action) + is CreateRoomAction.Create -> doCreateRoom() + } + } - fun setIsPublic(isPublic: Boolean) = setState { copy(isPublic = isPublic) } + private fun setName(action: CreateRoomAction.SetName) = setState { copy(roomName = action.name) } - fun setIsInRoomDirectory(isInRoomDirectory: Boolean) = setState { copy(isInRoomDirectory = isInRoomDirectory) } + private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState { copy(isPublic = action.isPublic) } - fun doCreateRoom() = withState { state -> + private fun setIsInRoomDirectory(action: CreateRoomAction.SetIsInRoomDirectory) = setState { copy(isInRoomDirectory = action.isInRoomDirectory) } + + private fun doCreateRoom() = withState { state -> if (state.asyncCreateRoomRequest is Loading || state.asyncCreateRoomRequest is Success) { return@withState } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerAction.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerAction.kt new file mode 100644 index 0000000000..157025bb0c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomdirectory.picker + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RoomDirectoryPickerAction : VectorViewModelAction { + object Retry : RoomDirectoryPickerAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt index f338c71bbf..ce7a57deba 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt @@ -19,17 +19,16 @@ package im.vector.riotx.features.roomdirectory.picker import android.os.Bundle import android.view.MenuItem import android.view.View -import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity -import im.vector.riotx.features.roomdirectory.RoomDirectoryNavigationViewModel +import im.vector.riotx.features.roomdirectory.RoomDirectoryAction +import im.vector.riotx.features.roomdirectory.RoomDirectorySharedAction +import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomdirectory.RoomDirectoryViewModel import kotlinx.android.synthetic.main.fragment_room_directory_picker.* import timber.log.Timber @@ -37,15 +36,14 @@ import javax.inject.Inject // TODO Set title to R.string.select_room_directory // TODO Menu to add custom room directory (not done in RiotWeb so far...) -class RoomDirectoryPickerFragment : VectorBaseFragment(), RoomDirectoryPickerController.Callback { +class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerViewModelFactory: RoomDirectoryPickerViewModel.Factory, + private val roomDirectoryPickerController: RoomDirectoryPickerController +) : VectorBaseFragment(), RoomDirectoryPickerController.Callback { private val viewModel: RoomDirectoryViewModel by activityViewModel() - private lateinit var navigationViewModel: RoomDirectoryNavigationViewModel + private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel private val pickerViewModel: RoomDirectoryPickerViewModel by fragmentViewModel() - @Inject lateinit var roomDirectoryPickerViewModelFactory: RoomDirectoryPickerViewModel.Factory - @Inject lateinit var roomDirectoryPickerController: RoomDirectoryPickerController - override fun getLayoutResId() = R.layout.fragment_room_directory_picker override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -57,6 +55,9 @@ class RoomDirectoryPickerFragment : VectorBaseFragment(), RoomDirectoryPickerCon it.setDisplayShowHomeEnabled(true) it.setDisplayHomeAsUpEnabled(true) } + + sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) + setupRecyclerView() } override fun getMenuRes() = R.menu.menu_directory_server_picker @@ -71,16 +72,6 @@ class RoomDirectoryPickerFragment : VectorBaseFragment(), RoomDirectoryPickerCon return super.onOptionsItemSelected(item) } - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - navigationViewModel = ViewModelProviders.of(requireActivity()).get(RoomDirectoryNavigationViewModel::class.java) - setupRecyclerView() - } - private fun setupRecyclerView() { val layoutManager = LinearLayoutManager(context) @@ -92,14 +83,14 @@ class RoomDirectoryPickerFragment : VectorBaseFragment(), RoomDirectoryPickerCon override fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData) { Timber.v("onRoomDirectoryClicked: $roomDirectoryData") - viewModel.setRoomDirectoryData(roomDirectoryData) + viewModel.handle(RoomDirectoryAction.SetRoomDirectoryData(roomDirectoryData)) - navigationViewModel.goTo(RoomDirectoryActivity.Navigation.Back) + sharedActionViewModel.post(RoomDirectorySharedAction.Back) } override fun retry() { Timber.v("Retry") - pickerViewModel.load() + pickerViewModel.handle(RoomDirectoryPickerAction.Retry) } override fun invalidate() = withState(pickerViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt index 6cc96a1f70..b7bdaea495 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt @@ -16,11 +16,7 @@ package im.vector.riotx.features.roomdirectory.picker -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback @@ -29,7 +25,8 @@ import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProt import im.vector.riotx.core.platform.VectorViewModel class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initialState: RoomDirectoryPickerViewState, - private val session: Session) : VectorViewModel(initialState) { + private val session: Session) + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -49,7 +46,7 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial load() } - fun load() { + private fun load() { session.getThirdPartyProtocol(object : MatrixCallback> { override fun onSuccess(data: Map) { setState { @@ -64,4 +61,10 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial } }) } + + override fun handle(action: RoomDirectoryPickerAction) { + when (action) { + RoomDirectoryPickerAction.Retry -> load() + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt new file mode 100644 index 0000000000..426078fa3d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomdirectory.roompreview + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RoomPreviewAction : VectorViewModelAction { + object Join : RoomPreviewAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt index e4fb122f7a..1bd138552e 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -67,9 +67,9 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable { if (args.worldReadable) { // TODO Room preview: Note: M does not recommend to use /events anymore, so for now we just display the room preview // TODO the same way if it was not world readable - addFragment(RoomPreviewNoPreviewFragment.newInstance(args), R.id.simpleFragmentContainer) + addFragment(R.id.simpleFragmentContainer, RoomPreviewNoPreviewFragment::class.java, args) } else { - addFragment(RoomPreviewNoPreviewFragment.newInstance(args), R.id.simpleFragmentContainer) + addFragment(R.id.simpleFragmentContainer, RoomPreviewNoPreviewFragment::class.java, args) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index e919b0096c..9003421dc7 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -19,13 +19,11 @@ package im.vector.riotx.features.roomdirectory.roompreview import android.os.Bundle import android.view.View import androidx.core.view.isVisible -import androidx.fragment.app.Fragment import androidx.transition.TransitionManager import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.platform.ButtonStateView @@ -38,34 +36,20 @@ import javax.inject.Inject /** * Note: this Fragment is also used for world readable room for the moment */ -class RoomPreviewNoPreviewFragment : VectorBaseFragment() { +class RoomPreviewNoPreviewFragment @Inject constructor( + private val errorFormatter: ErrorFormatter, + val roomPreviewViewModelFactory: RoomPreviewViewModel.Factory, + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment() { - companion object { - fun newInstance(arg: RoomPreviewData): Fragment { - return RoomPreviewNoPreviewFragment().apply { setArguments(arg) } - } - } - - @Inject lateinit var errorFormatter: ErrorFormatter - @Inject lateinit var roomPreviewViewModelFactory: RoomPreviewViewModel.Factory - @Inject lateinit var avatarRenderer: AvatarRenderer private val roomPreviewViewModel: RoomPreviewViewModel by fragmentViewModel() private val roomPreviewData: RoomPreviewData by args() - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - setupToolbar(roomPreviewNoPreviewToolbar) - } - override fun getLayoutResId() = R.layout.fragment_room_preview_no_preview override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - + setupToolbar(roomPreviewNoPreviewToolbar) // Toolbar avatarRenderer.render(roomPreviewData.avatarUrl, roomPreviewData.roomId, roomPreviewData.roomName, roomPreviewNoPreviewToolbarAvatar) roomPreviewNoPreviewToolbarTitle.text = roomPreviewData.roomName @@ -83,7 +67,7 @@ class RoomPreviewNoPreviewFragment : VectorBaseFragment() { roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback { override fun onButtonClicked() { - roomPreviewViewModel.joinRoom() + roomPreviewViewModel.handle(RoomPreviewAction.Join) } override fun onRetryClicked() { diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index 2bd75c2b90..9ffb64556f 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -30,7 +30,8 @@ import im.vector.riotx.features.roomdirectory.JoinState import timber.log.Timber class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: RoomPreviewViewState, - private val session: Session) : VectorViewModel(initialState) { + private val session: Session) + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -76,7 +77,13 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R .disposeOnClear() } - fun joinRoom() = withState { state -> + override fun handle(action: RoomPreviewAction) { + when (action) { + RoomPreviewAction.Join -> joinRoom() + } + } + + private fun joinRoom() = withState { state -> if (state.roomJoinState == JoinState.JOINING) { // Request already sent, should not happen Timber.w("Try to join an already joining room. Should not happen") diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt index 7fef12cddf..93931fe71d 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt @@ -21,12 +21,16 @@ import android.content.res.Configuration import android.os.Build import android.preference.PreferenceManager import androidx.core.content.edit +import im.vector.riotx.BuildConfig import im.vector.riotx.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber import java.util.Locale +import kotlin.Comparator +import kotlin.collections.ArrayList +import kotlin.collections.HashSet /** * Object to manage the Locale choice of the user @@ -35,6 +39,7 @@ object VectorLocale { private const val APPLICATION_LOCALE_COUNTRY_KEY = "APPLICATION_LOCALE_COUNTRY_KEY" private const val APPLICATION_LOCALE_VARIANT_KEY = "APPLICATION_LOCALE_VARIANT_KEY" private const val APPLICATION_LOCALE_LANGUAGE_KEY = "APPLICATION_LOCALE_LANGUAGE_KEY" + private const val APPLICATION_LOCALE_SCRIPT_KEY = "APPLICATION_LOCALE_SCRIPT_KEY" private val defaultLocale = Locale("en", "US") @@ -106,6 +111,15 @@ object VectorLocale { } else { putString(APPLICATION_LOCALE_VARIANT_KEY, variant) } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val script = locale.script + if (script.isEmpty()) { + remove(APPLICATION_LOCALE_SCRIPT_KEY) + } else { + putString(APPLICATION_LOCALE_SCRIPT_KEY, script) + } + } } } @@ -159,24 +173,43 @@ object VectorLocale { * @param context the context */ private fun initApplicationLocales(context: Context) { - val knownLocalesSet = HashSet>() + val knownLocalesSet = HashSet>() try { val availableLocales = Locale.getAvailableLocales() for (locale in availableLocales) { - knownLocalesSet.add(Pair(getString(context, locale, R.string.resources_language), - getString(context, locale, R.string.resources_country_code))) + knownLocalesSet.add( + Triple( + getString(context, locale, R.string.resources_language), + getString(context, locale, R.string.resources_country_code), + getString(context, locale, R.string.resources_script) + ) + ) } } catch (e: Exception) { Timber.e(e, "## getApplicationLocales() : failed") - knownLocalesSet.add(Pair(context.getString(R.string.resources_language), context.getString(R.string.resources_country_code))) + knownLocalesSet.add( + Triple( + context.getString(R.string.resources_language), + context.getString(R.string.resources_country_code), + context.getString(R.string.resources_script) + ) + ) } supportedLocales.clear() - knownLocalesSet.mapTo(supportedLocales) { (language, country) -> - Locale(language, country) + knownLocalesSet.mapTo(supportedLocales) { (language, country, script) -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Locale.Builder() + .setLanguage(language) + .setRegion(country) + .setScript(script) + .build() + } else { + Locale(language, country) + } } // sort by human display names @@ -190,12 +223,37 @@ object VectorLocale { * @return the string */ fun localeToLocalisedString(locale: Locale): String { - var res = locale.getDisplayLanguage(locale) + return buildString { + append(locale.getDisplayLanguage(locale)) - if (locale.getDisplayCountry(locale).isNotEmpty()) { - res += " (" + locale.getDisplayCountry(locale) + ")" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && locale.script != "Latn" + && locale.getDisplayScript(locale).isNotEmpty()) { + append(" - ") + append(locale.getDisplayScript(locale)) + } + + if (locale.getDisplayCountry(locale).isNotEmpty()) { + append(" (") + append(locale.getDisplayCountry(locale)) + append(")") + } + + // In debug mode, also display information about the locale in the current locale. + if (BuildConfig.DEBUG) { + append("\n[") + append(locale.displayLanguage) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") { + append(" - ") + append(locale.displayScript) + } + if (locale.displayCountry.isNotEmpty()) { + append(" (") + append(locale.displayCountry) + append(")") + } + append("]") + } } - - return res } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index f9601265d3..dd99488465 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -55,8 +55,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { const val SETTINGS_CONTACT_PREFERENCE_KEYS = "SETTINGS_CONTACT_PREFERENCE_KEYS" const val SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY" const val SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY" - const val SETTINGS_IGNORED_USERS_PREFERENCE_KEY = "SETTINGS_IGNORED_USERS_PREFERENCE_KEY" - const val SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY = "SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY" const val SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY" const val SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY" const val SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY" @@ -98,6 +96,9 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_VIBRATE_ON_MENTION_KEY = "SETTINGS_VIBRATE_ON_MENTION_KEY" private const val SETTINGS_SEND_MESSAGE_WITH_ENTER = "SETTINGS_SEND_MESSAGE_WITH_ENTER" + // Help + private const val SETTINGS_SHOULD_SHOW_HELP_ON_ROOM_LIST_KEY = "SETTINGS_SHOULD_SHOW_HELP_ON_ROOM_LIST_KEY" + // home private const val SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY = "SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY" private const val SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY = "SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY" @@ -544,7 +545,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { MEDIA_SAVING_1_WEEK -> System.currentTimeMillis() / 1000 - 7 * 24 * 60 * 60 MEDIA_SAVING_1_MONTH -> System.currentTimeMillis() / 1000 - 30 * 24 * 60 * 60 MEDIA_SAVING_FOREVER -> 0 - else -> 0 + else -> 0 } } @@ -559,7 +560,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { MEDIA_SAVING_1_WEEK -> context.getString(R.string.media_saving_period_1_week) MEDIA_SAVING_1_MONTH -> context.getString(R.string.media_saving_period_1_month) MEDIA_SAVING_FOREVER -> context.getString(R.string.media_saving_period_forever) - else -> "?" + else -> "?" } } @@ -576,7 +577,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { * @return true if the markdown is enabled */ fun isMarkdownEnabled(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, true) + return defaultPrefs.getBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, false) } /** @@ -599,6 +600,24 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, true) } + /** + * Tells if the help on room list should be shown + * + * @return true if the help on room list should be shown + */ + fun shouldShowLongClickOnRoomHelp(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SHOULD_SHOW_HELP_ON_ROOM_LIST_KEY, true) + } + + /** + * Prevent help on room list to be shown again + */ + fun neverShowLongClickOnRoomHelpAgain() { + defaultPrefs.edit { + putBoolean(SETTINGS_SHOULD_SHOW_HELP_ON_ROOM_LIST_KEY, false) + } + } + /** * Tells if the message timestamps must be always shown * diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index cd74dd7016..16484224af 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -23,6 +23,7 @@ import androidx.preference.PreferenceFragmentCompat import im.vector.matrix.android.api.session.Session import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.VectorBaseActivity import kotlinx.android.synthetic.main.activity_vector_settings.* import timber.log.Timber @@ -52,11 +53,8 @@ class VectorSettingsActivity : VectorBaseActivity(), configureToolbar(settingsToolbar) if (isFirstCreation()) { - val vectorSettingsPreferencesFragment = VectorSettingsRootFragment.newInstance() // display the fragment - supportFragmentManager.beginTransaction() - .replace(R.id.vector_settings_page, vectorSettingsPreferencesFragment, FRAGMENT_TAG) - .commit() + replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) } supportFragmentManager.addOnBackStackChangedListener(this) @@ -76,9 +74,9 @@ class VectorSettingsActivity : VectorBaseActivity(), override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean { val oFragment = when { VectorPreferences.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref.key -> - VectorSettingsNotificationsTroubleshootFragment.newInstance() + supportFragmentManager.fragmentFactory.instantiate(classLoader, VectorSettingsNotificationsTroubleshootFragment::class.java.name) VectorPreferences.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY == pref.key -> - VectorSettingsAdvancedNotificationPreferenceFragment.newInstance() + supportFragmentManager.fragmentFactory.instantiate(classLoader, VectorSettingsAdvancedNotificationPreferenceFragment::class.java.name) else -> try { pref.fragment?.let { 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 380e043ecf..a8328fae52 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 @@ -24,14 +24,15 @@ import androidx.core.content.edit import androidx.preference.Preference import androidx.preference.PreferenceManager import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.preference.BingRule import im.vector.riotx.core.preference.BingRulePreference import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.features.notifications.NotificationUtils import javax.inject.Inject -class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseFragment() { +class VectorSettingsAdvancedNotificationPreferenceFragment @Inject constructor( + private val vectorPreferences: VectorPreferences +) : VectorSettingsBaseFragment() { // events listener /* TODO @@ -46,12 +47,6 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF override val preferenceXmlRes = R.xml.vector_settings_notification_advanced_preferences - @Inject lateinit var vectorPreferences: VectorPreferences - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun bindPref() { val callNotificationsSystemOptions = findPreference(VectorPreferences.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY)!! if (NotificationUtils.supportNotificationChannels()) { @@ -229,7 +224,5 @@ class VectorSettingsAdvancedNotificationPreferenceFragment : VectorSettingsBaseF VectorPreferences.SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY to BingRule.RULE_ID_CALL, VectorPreferences.SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY to BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS ) - - fun newInstance() = VectorSettingsAdvancedNotificationPreferenceFragment() } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 331b6e935a..ff76c61754 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -51,7 +51,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File -import java.util.* class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt index a05ccb305d..6ce928c05d 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsHelpAboutFragment.kt @@ -23,24 +23,19 @@ import androidx.preference.Preference import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import im.vector.matrix.android.api.Matrix import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.core.utils.copyToClipboard import im.vector.riotx.core.utils.displayInWebView import im.vector.riotx.features.version.VersionProvider import javax.inject.Inject -class VectorSettingsHelpAboutFragment : VectorSettingsBaseFragment() { +class VectorSettingsHelpAboutFragment @Inject constructor( + private val versionProvider: VersionProvider +) : VectorSettingsBaseFragment() { override var titleRes = R.string.preference_root_help_about override val preferenceXmlRes = R.xml.vector_settings_help_about - @Inject lateinit var versionProvider: VersionProvider - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun bindPref() { // preference to start the App info screen, to facilitate App permissions access findPreference(APP_INFO_LINK_PREFERENCE_KEY)!! diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsIgnoredUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsIgnoredUsersFragment.kt deleted file mode 100644 index 32a6fe8f41..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsIgnoredUsersFragment.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.features.settings - -import androidx.appcompat.app.AlertDialog -import androidx.preference.Preference -import androidx.preference.PreferenceCategory -import im.vector.riotx.R -import im.vector.riotx.core.preference.VectorPreference -import java.util.ArrayList -import kotlin.Comparator - -class VectorSettingsIgnoredUsersFragment : VectorSettingsBaseFragment() { - - override var titleRes = R.string.settings_ignored_users - override val preferenceXmlRes = R.xml.vector_settings_ignored_users - - // displayed the ignored users list - private val mIgnoredUserSettingsCategoryDivider by lazy { - findPreference(VectorPreferences.SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY)!! - } - private val mIgnoredUserSettingsCategory by lazy { - findPreference(VectorPreferences.SETTINGS_IGNORED_USERS_PREFERENCE_KEY)!! - } - - override fun bindPref() { - // Ignore users - refreshIgnoredUsersList() - } - - // ============================================================================================================== - // ignored users list management - // ============================================================================================================== - - /** - * Refresh the ignored users list - */ - private fun refreshIgnoredUsersList() { - val ignoredUsersList = mutableListOf() // TODO session.dataHandler.ignoredUserIds - - ignoredUsersList.sortWith(Comparator { u1, u2 -> - u1.toLowerCase(VectorLocale.applicationLocale).compareTo(u2.toLowerCase(VectorLocale.applicationLocale)) - }) - - val preferenceScreen = preferenceScreen - - preferenceScreen.removePreference(mIgnoredUserSettingsCategory) - preferenceScreen.removePreference(mIgnoredUserSettingsCategoryDivider) - mIgnoredUserSettingsCategory.removeAll() - - if (ignoredUsersList.size > 0) { - preferenceScreen.addPreference(mIgnoredUserSettingsCategoryDivider) - preferenceScreen.addPreference(mIgnoredUserSettingsCategory) - - for (userId in ignoredUsersList) { - val preference = Preference(activity) - - preference.title = userId - preference.key = IGNORED_USER_KEY_BASE + userId - - preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - activity?.let { - AlertDialog.Builder(it) - .setMessage(getString(R.string.settings_unignore_user, userId)) - .setPositiveButton(R.string.yes) { _, _ -> - displayLoadingView() - - val idsList = ArrayList() - idsList.add(userId) - - notImplemented() - /* TODO - session.unIgnoreUsers(idsList, object : MatrixCallback { - override fun onSuccess(info: Void?) { - onCommonDone(null) - } - - override fun onNetworkError(e: Exception) { - onCommonDone(e.localizedMessage) - } - - override fun onMatrixError(e: MatrixError) { - onCommonDone(e.localizedMessage) - } - - override fun onUnexpectedError(e: Exception) { - onCommonDone(e.localizedMessage) - } - }) - */ - } - .setNegativeButton(R.string.no, null) - .show() - } - - false - } - - mIgnoredUserSettingsCategory.addPreference(preference) - } - } - } - - companion object { - private const val IGNORED_USER_KEY_BASE = "IGNORED_USER_KEY_BASE" - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationPreferenceFragment.kt index cfb4c88259..b397cd1cf6 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationPreferenceFragment.kt @@ -24,22 +24,21 @@ 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 import im.vector.riotx.core.preference.VectorSwitchPreference import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.push.fcm.FcmHelper import javax.inject.Inject // Referenced in vector_settings_preferences_root.xml -class VectorSettingsNotificationPreferenceFragment : VectorSettingsBaseFragment() { +class VectorSettingsNotificationPreferenceFragment @Inject constructor( + private val pushManager: PushersManager, + private val activeSessionHolder: ActiveSessionHolder, + private val vectorPreferences: VectorPreferences +) : VectorSettingsBaseFragment() { override var titleRes: Int = R.string.settings_notifications override val preferenceXmlRes = R.xml.vector_settings_notifications - @Inject lateinit var pushManager: PushersManager - @Inject lateinit var activeSessionHolder: ActiveSessionHolder - @Inject lateinit var vectorPreferences: VectorPreferences - override fun bindPref() { findPreference(VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY)!!.let { pref -> val pushRuleService = session @@ -57,10 +56,6 @@ class VectorSettingsNotificationPreferenceFragment : VectorSettingsBaseFragment( } } - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onResume() { super.onResume() activeSessionHolder.getSafeActiveSession()?.refreshPushers() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt index 5737425a3b..6f43114eb4 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt @@ -28,9 +28,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import butterknife.BindView -import im.vector.matrix.android.api.session.Session import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.rageshake.BugReporter @@ -39,7 +37,10 @@ import im.vector.riotx.features.settings.troubleshoot.TroubleshootTest import im.vector.riotx.push.fcm.NotificationTroubleshootTestManagerFactory import javax.inject.Inject -class VectorSettingsNotificationsTroubleshootFragment : VectorBaseFragment() { +class VectorSettingsNotificationsTroubleshootFragment @Inject constructor( + private val bugReporter: BugReporter, + private val testManagerFactory: NotificationTroubleshootTestManagerFactory +) : VectorBaseFragment() { @BindView(R.id.troubleshoot_test_recycler_view) lateinit var mRecyclerView: RecyclerView @@ -54,18 +55,11 @@ class VectorSettingsNotificationsTroubleshootFragment : VectorBaseFragment() { private var testManager: NotificationTroubleshootTestManager? = null // members - @Inject lateinit var session: Session - @Inject lateinit var bugReporter: BugReporter - @Inject lateinit var testManagerFactory: NotificationTroubleshootTestManagerFactory override fun getLayoutResId() = R.layout.fragment_settings_notifications_troubleshoot private var interactionListener: VectorSettingsFragmentInteractionListener? = null - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -73,7 +67,7 @@ class VectorSettingsNotificationsTroubleshootFragment : VectorBaseFragment() { mRecyclerView.layoutManager = layoutManager val dividerItemDecoration = DividerItemDecoration(mRecyclerView.context, - layoutManager.orientation) + layoutManager.orientation) mRecyclerView.addItemDecoration(dividerItemDecoration) mSummaryButton.setOnClickListener { @@ -88,7 +82,7 @@ class VectorSettingsNotificationsTroubleshootFragment : VectorBaseFragment() { private fun startUI() { mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_running_status, - 0, 0) + 0, 0) testManager = testManagerFactory.create(this) testManager?.statusListener = { troubleshootTestManager -> if (isAdded) { @@ -167,9 +161,4 @@ class VectorSettingsNotificationsTroubleshootFragment : VectorBaseFragment() { interactionListener = context } } - - companion object { - // static constructor - fun newInstance() = VectorSettingsNotificationsTroubleshootFragment() - } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt index ab81e6937e..9c240ad093 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt @@ -25,14 +25,16 @@ import androidx.appcompat.app.AlertDialog import androidx.preference.Preference import androidx.preference.SwitchPreference import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.preference.VectorListPreference import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.themes.ThemeUtils import javax.inject.Inject -class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() { +class VectorSettingsPreferencesFragment @Inject constructor( + private val vectorConfiguration: VectorConfiguration, + private val vectorPreferences: VectorPreferences +) : VectorSettingsBaseFragment() { override var titleRes = R.string.settings_preferences override val preferenceXmlRes = R.xml.vector_settings_preferences @@ -44,13 +46,6 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() { findPreference(VectorPreferences.SETTINGS_INTERFACE_TEXT_SIZE_KEY)!! } - @Inject lateinit var vectorConfiguration: VectorConfiguration - @Inject lateinit var vectorPreferences: VectorPreferences - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun bindPref() { // user interface preferences setUserInterfacePreferences() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsRootFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsRootFragment.kt index a70164485f..a50e0e9ad0 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsRootFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsRootFragment.kt @@ -17,9 +17,9 @@ package im.vector.riotx.features.settings import im.vector.riotx.R -import im.vector.riotx.core.extensions.withArgs +import javax.inject.Inject -class VectorSettingsRootFragment : VectorSettingsBaseFragment() { +class VectorSettingsRootFragment @Inject constructor() : VectorSettingsBaseFragment() { override var titleRes: Int = R.string.title_activity_settings override val preferenceXmlRes = R.xml.vector_settings_root @@ -27,11 +27,4 @@ class VectorSettingsRootFragment : VectorSettingsBaseFragment() { override fun bindPref() { // Nothing to do } - - companion object { - fun newInstance() = VectorSettingsRootFragment() - .withArgs { - // putString(ARG_MATRIX_ID, matrixId) - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 2f52cdef13..b7ec443ea0 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -40,7 +40,6 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.dialogs.ExportKeysDialog import im.vector.riotx.core.intent.ExternalIntentData import im.vector.riotx.core.intent.analyseIntent @@ -56,10 +55,13 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActiv import timber.log.Timber import java.text.DateFormat import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale import javax.inject.Inject -class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { +class VectorSettingsSecurityPrivacyFragment @Inject constructor( + private val vectorPreferences: VectorPreferences +) : VectorSettingsBaseFragment() { override var titleRes = R.string.settings_security_and_privacy override val preferenceXmlRes = R.xml.vector_settings_security_privacy @@ -127,12 +129,6 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() { findPreference(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!! } - @Inject lateinit var vectorPreferences: VectorPreferences - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun bindPref() { // Push target refreshPushersList() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/ignored/IgnoredUsersController.kt b/vector/src/main/java/im/vector/riotx/features/settings/ignored/IgnoredUsersController.kt new file mode 100644 index 0000000000..120781874d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/ignored/IgnoredUsersController.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.ignored + +import com.airbnb.epoxy.EpoxyController +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.noResultItem +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +class IgnoredUsersController @Inject constructor(private val stringProvider: StringProvider, + private val avatarRenderer: AvatarRenderer) : EpoxyController() { + + var callback: Callback? = null + private var viewState: IgnoredUsersViewState? = null + + init { + requestModelBuild() + } + + fun update(viewState: IgnoredUsersViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val nonNullViewState = viewState ?: return + buildIgnoredUserModels(nonNullViewState.ignoredUsers) + } + + private fun buildIgnoredUserModels(userIds: List) { + if (userIds.isEmpty()) { + noResultItem { + id("empty") + text(stringProvider.getString(R.string.no_ignored_users)) + } + } else { + userIds.forEach { userId -> + userItem { + id(userId.userId) + avatarRenderer(avatarRenderer) + user(userId) + itemClickAction { callback?.onUserIdClicked(userId.userId) } + } + } + } + } + + interface Callback { + fun onUserIdClicked(userId: String) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/ignored/IgnoredUsersViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/ignored/IgnoredUsersViewModel.kt new file mode 100644 index 0000000000..e77660746c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/ignored/IgnoredUsersViewModel.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.ignored + +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.session.Session +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.postLiveEvent +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction + +data class IgnoredUsersViewState( + val ignoredUsers: List = emptyList(), + val unIgnoreRequest: Async = Uninitialized +) : MvRxState + +sealed class IgnoredUsersAction : VectorViewModelAction { + data class UnIgnore(val userId: String) : IgnoredUsersAction() +} + +class IgnoredUsersViewModel @AssistedInject constructor(@Assisted initialState: IgnoredUsersViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: IgnoredUsersViewState): IgnoredUsersViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: IgnoredUsersViewState): IgnoredUsersViewModel? { + val ignoredUsersFragment: VectorSettingsIgnoredUsersFragment = (viewModelContext as FragmentViewModelContext).fragment() + return ignoredUsersFragment.ignoredUsersViewModelFactory.create(state) + } + } + + init { + observeIgnoredUsers() + } + + private fun observeIgnoredUsers() { + session.rx() + .liveIgnoredUsers() + .execute { async -> + copy( + ignoredUsers = async.invoke().orEmpty() + ) + } + } + + override fun handle(action: IgnoredUsersAction) { + when (action) { + is IgnoredUsersAction.UnIgnore -> handleUnIgnore(action) + } + } + + private fun handleUnIgnore(action: IgnoredUsersAction.UnIgnore) { + setState { + copy( + unIgnoreRequest = Loading() + ) + } + + session.unIgnoreUserIds(listOf(action.userId), object : MatrixCallback { + override fun onFailure(failure: Throwable) { + setState { + copy( + unIgnoreRequest = Fail(failure) + ) + } + + _requestErrorLiveData.postLiveEvent(failure) + } + + override fun onSuccess(data: Unit) { + setState { + copy( + unIgnoreRequest = Success(data) + ) + } + } + }) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/ignored/UserItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/ignored/UserItem.kt new file mode 100644 index 0000000000..a9c1b98915 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/ignored/UserItem.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.settings.ignored + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.home.AvatarRenderer + +/** + * A list item for User. + */ +@EpoxyModelClass(layout = R.layout.item_user) +abstract class UserItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + lateinit var user: User + + @EpoxyAttribute + var itemClickAction: (() -> Unit)? = null + + override fun bind(holder: Holder) { + holder.root.setOnClickListener { itemClickAction?.invoke() } + + avatarRenderer.render(user, holder.avatarImage) + holder.userIdText.setTextOrHide(user.userId) + holder.displayNameText.setTextOrHide(user.displayName) + } + + class Holder : VectorEpoxyHolder() { + val root by bind(R.id.itemUserRoot) + val avatarImage by bind(R.id.itemUserAvatar) + val userIdText by bind(R.id.itemUserId) + val displayNameText by bind(R.id.itemUserName) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt new file mode 100644 index 0000000000..11e473ae24 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.ignored + +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* +import javax.inject.Inject + +class VectorSettingsIgnoredUsersFragment @Inject constructor( + val ignoredUsersViewModelFactory: IgnoredUsersViewModel.Factory, + private val ignoredUsersController: IgnoredUsersController, + private val errorFormatter: ErrorFormatter +) : VectorBaseFragment(), IgnoredUsersController.Callback { + + override fun getLayoutResId() = R.layout.fragment_generic_recycler_epoxy + + private val ignoredUsersViewModel: IgnoredUsersViewModel by fragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + waiting_view_status_text.setText(R.string.please_wait) + waiting_view_status_text.isVisible = true + ignoredUsersController.callback = this + epoxyRecyclerView.setController(ignoredUsersController) + ignoredUsersViewModel.requestErrorLiveData.observeEvent(this) { + displayErrorDialog(it) + } + } + + override fun onResume() { + super.onResume() + + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_ignored_users) + } + + override fun onUserIdClicked(userId: String) { + AlertDialog.Builder(requireActivity()) + .setMessage(getString(R.string.settings_unignore_user, userId)) + .setPositiveButton(R.string.yes) { _, _ -> + ignoredUsersViewModel.handle(IgnoredUsersAction.UnIgnore(userId)) + } + .setNegativeButton(R.string.no, null) + .show() + } + + private fun displayErrorDialog(throwable: Throwable) { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(throwable)) + .setPositiveButton(R.string.ok, null) + .show() + } + + // ============================================================================================================== + // ignored users list management + // ============================================================================================================== + + override fun invalidate() = withState(ignoredUsersViewModel) { state -> + ignoredUsersController.update(state) + + handleUnIgnoreRequestStatus(state.unIgnoreRequest) + } + + private fun handleUnIgnoreRequestStatus(unIgnoreRequest: Async) { + when (unIgnoreRequest) { + is Loading -> waiting_view.isVisible = true + else -> waiting_view.isVisible = false + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysFragment.kt index c20ba2e248..ea23ba2583 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysFragment.kt @@ -24,7 +24,6 @@ import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.StringProvider @@ -33,11 +32,12 @@ import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* import javax.inject.Inject // Referenced in vector_settings_notifications.xml -class PushGatewaysFragment : VectorBaseFragment() { +class PushGatewaysFragment @Inject constructor( + val pushGatewaysViewModelFactory: PushGatewaysViewModel.Factory +) : VectorBaseFragment() { override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy - @Inject lateinit var pushGatewaysViewModelFactory: PushGatewaysViewModel.Factory private val viewModel: PushGatewaysViewModel by fragmentViewModel(PushGatewaysViewModel::class) private val epoxyController by lazy { PushGateWayController(StringProvider(requireContext().resources)) } @@ -46,10 +46,6 @@ class PushGatewaysFragment : VectorBaseFragment() { (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_notifications_targets) } - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) val lmgr = LinearLayoutManager(context, RecyclerView.VERTICAL, false) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysViewModel.kt index c51da409cf..dd773f4c22 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysViewModel.kt @@ -16,17 +16,13 @@ package im.vector.riotx.features.settings.push -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Uninitialized -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.rx.RxSession +import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.VectorViewModel data class PushGatewayViewState( @@ -34,7 +30,8 @@ data class PushGatewayViewState( ) : MvRxState class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: PushGatewayViewState, - private val session: Session) : VectorViewModel(initialState) { + private val session: Session) + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -60,4 +57,8 @@ class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: copy(pushGateways = it) } } + + override fun handle(action: EmptyAction) { + // No op + } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt index d651deaf80..e376b7ed6f 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt @@ -20,13 +20,15 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.riotx.core.di.HasScreenInjector +import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.VectorViewModel data class PushRulesViewState( val rules: List = emptyList() ) : MvRxState -class PushRulesViewModel(initialState: PushRulesViewState) : VectorViewModel(initialState) { +class PushRulesViewModel(initialState: PushRulesViewState) + : VectorViewModel(initialState) { companion object : MvRxViewModelFactory { @@ -36,4 +38,8 @@ class PushRulesViewModel(initialState: PushRulesViewState) : VectorViewModel) { - val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Attachments(attachments)) - roomListFragment = RoomListFragment.newInstance(roomListParams) - .also { replaceFragment(it, R.id.shareRoomListFragmentContainer) } + val roomListParams = RoomListParams(RoomListDisplayMode.SHARE, sharedData = SharedData.Attachments(attachments)) + replaceFragment(R.id.shareRoomListFragmentContainer, RoomListFragment::class.java, roomListParams) } override fun onAttachmentsProcessFailed() { @@ -115,9 +117,8 @@ class IncomingShareActivity : return if (sharedText.isNullOrEmpty()) { false } else { - val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Text(sharedText)) - roomListFragment = RoomListFragment.newInstance(roomListParams) - .also { replaceFragment(it, R.id.shareRoomListFragmentContainer) } + val roomListParams = RoomListParams(RoomListDisplayMode.SHARE, sharedData = SharedData.Text(sharedText)) + replaceFragment(R.id.shareRoomListFragmentContainer, RoomListFragment::class.java, roomListParams) true } } diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt index 51485ecbf9..72c98cdc45 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt @@ -23,7 +23,8 @@ import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.rx.rx -import im.vector.riotx.ActiveSessionObservableStore +import im.vector.riotx.ActiveSessionDataSource +import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.VectorViewModel import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers @@ -35,9 +36,9 @@ data class IncomingShareState(private val dummy: Boolean = false) : MvRxState * View model used to observe the room list and post update to the ShareRoomListObservableStore */ class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: IncomingShareState, - private val sessionObservableStore: ActiveSessionObservableStore, - private val shareRoomListObservableStore: ShareRoomListObservableStore) - : VectorViewModel(initialState) { + private val sessionObservableStore: ActiveSessionDataSource, + private val shareRoomListObservableStore: ShareRoomListDataSource) + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -70,4 +71,8 @@ class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState: } .disposeOnClear() } + + override fun handle(action: EmptyAction) { + // No op + } } diff --git a/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListObservableStore.kt b/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListDataSource.kt similarity index 83% rename from vector/src/main/java/im/vector/riotx/features/share/ShareRoomListObservableStore.kt rename to vector/src/main/java/im/vector/riotx/features/share/ShareRoomListDataSource.kt index c46ec42d64..b1b4d7b46e 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListObservableStore.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/ShareRoomListDataSource.kt @@ -17,9 +17,9 @@ package im.vector.riotx.features.share import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.riotx.core.utils.RxStore +import im.vector.riotx.core.utils.BehaviorDataSource import javax.inject.Inject import javax.inject.Singleton @Singleton -class ShareRoomListObservableStore @Inject constructor() : RxStore>() +class ShareRoomListDataSource @Inject constructor() : BehaviorDataSource>() diff --git a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt index 2edb59104b..40a14b3e6f 100644 --- a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt @@ -28,7 +28,6 @@ import androidx.core.graphics.drawable.DrawableCompat import androidx.preference.PreferenceManager import im.vector.riotx.R import timber.log.Timber -import java.util.* /** * Util class for managing themes. @@ -131,24 +130,16 @@ object ThemeUtils { */ @ColorInt fun getColor(c: Context, @AttrRes colorAttribute: Int): Int { - if (mColorByAttr.containsKey(colorAttribute)) { - return mColorByAttr[colorAttribute] as Int + return mColorByAttr.getOrPut(colorAttribute) { + try { + val color = TypedValue() + c.theme.resolveAttribute(colorAttribute, color, true) + color.data + } catch (e: Exception) { + Timber.e(e, "Unable to get color") + ContextCompat.getColor(c, android.R.color.holo_red_dark) + } } - - var matchedColor: Int - - try { - val color = TypedValue() - c.theme.resolveAttribute(colorAttribute, color, true) - matchedColor = color.data - } catch (e: Exception) { - Timber.e(e, "Unable to get color") - matchedColor = ContextCompat.getColor(c, android.R.color.holo_red_dark) - } - - mColorByAttr[colorAttribute] = matchedColor - - return matchedColor } fun getAttribute(c: Context, @AttrRes attribute: Int): TypedValue? { diff --git a/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt b/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt index 85051a0137..f850d12422 100644 --- a/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt +++ b/vector/src/main/java/im/vector/riotx/features/ui/SharedPreferencesUiStateRepository.kt @@ -18,7 +18,7 @@ package im.vector.riotx.features.ui import android.content.SharedPreferences import androidx.core.content.edit -import im.vector.riotx.features.home.room.list.RoomListFragment +import im.vector.riotx.features.home.RoomListDisplayMode import javax.inject.Inject /** @@ -26,21 +26,21 @@ import javax.inject.Inject */ class SharedPreferencesUiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) : UiStateRepository { - override fun getDisplayMode(): RoomListFragment.DisplayMode { + override fun getDisplayMode(): RoomListDisplayMode { return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) { - VALUE_DISPLAY_MODE_PEOPLE -> RoomListFragment.DisplayMode.PEOPLE - VALUE_DISPLAY_MODE_ROOMS -> RoomListFragment.DisplayMode.ROOMS - else -> RoomListFragment.DisplayMode.HOME + VALUE_DISPLAY_MODE_PEOPLE -> RoomListDisplayMode.PEOPLE + VALUE_DISPLAY_MODE_ROOMS -> RoomListDisplayMode.ROOMS + else -> RoomListDisplayMode.HOME } } - override fun storeDisplayMode(displayMode: RoomListFragment.DisplayMode) { + override fun storeDisplayMode(displayMode: RoomListDisplayMode) { sharedPreferences.edit { putInt(KEY_DISPLAY_MODE, when (displayMode) { - RoomListFragment.DisplayMode.PEOPLE -> VALUE_DISPLAY_MODE_PEOPLE - RoomListFragment.DisplayMode.ROOMS -> VALUE_DISPLAY_MODE_ROOMS - else -> VALUE_DISPLAY_MODE_CATCHUP + RoomListDisplayMode.PEOPLE -> VALUE_DISPLAY_MODE_PEOPLE + RoomListDisplayMode.ROOMS -> VALUE_DISPLAY_MODE_ROOMS + else -> VALUE_DISPLAY_MODE_CATCHUP }) } } diff --git a/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt b/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt index 1c59a22892..feac6a64ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt +++ b/vector/src/main/java/im/vector/riotx/features/ui/UiStateRepository.kt @@ -16,14 +16,14 @@ package im.vector.riotx.features.ui -import im.vector.riotx.features.home.room.list.RoomListFragment +import im.vector.riotx.features.home.RoomListDisplayMode /** * This interface is used to persist UI state across application restart */ interface UiStateRepository { - fun getDisplayMode(): RoomListFragment.DisplayMode + fun getDisplayMode(): RoomListDisplayMode - fun storeDisplayMode(displayMode: RoomListFragment.DisplayMode) + fun storeDisplayMode(displayMode: RoomListDisplayMode) } diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt index 32d67b87ca..94c718466f 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.workers.signout import android.app.Activity import android.app.Dialog -import android.content.Context import android.content.Intent import android.os.Bundle import android.view.LayoutInflater @@ -31,27 +30,19 @@ import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders import androidx.transition.TransitionManager import butterknife.BindView import butterknife.ButterKnife import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.riotx.R -import im.vector.riotx.core.di.DaggerScreenComponent -import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.utils.toast import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity -class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() { - - lateinit var session: Session - lateinit var viewModelFactory: ViewModelProvider.Factory +class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { @BindView(R.id.bottom_sheet_signout_warning_text) lateinit var sheetTitle: TextView @@ -97,20 +88,10 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() { private lateinit var viewModel: SignOutViewModel - override fun onAttach(context: Context) { - super.onAttach(context) - val vectorBaseActivity = activity as VectorBaseActivity - val screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) - viewModelFactory = screenComponent.viewModelFactory() - session = screenComponent.activeSessionHolder().getActiveSession() - } - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = ViewModelProviders.of(this, viewModelFactory).get(SignOutViewModel::class.java) - - viewModel.init(session) + viewModel = fragmentViewModelProvider.get(SignOutViewModel::class.java) setupClickableView.setOnClickListener { context?.let { context -> @@ -162,7 +143,7 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() { } } - viewModel.keysExportedToFile.observe(this, Observer { + viewModel.keysExportedToFile.observe(viewLifecycleOwner, Observer { val hasExportedToFile = it ?: false if (hasExportedToFile) { // We can allow to sign out @@ -177,7 +158,7 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() { } }) - viewModel.keysBackupState.observe(this, Observer { + viewModel.keysBackupState.observe(viewLifecycleOwner, Observer { if (viewModel.keysExportedToFile.value == true) { // ignore this return@Observer diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt index c32d6f6631..8964b29952 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt @@ -23,59 +23,43 @@ import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener import javax.inject.Inject -class SignOutViewModel @Inject constructor() : ViewModel(), KeysBackupStateListener { +class SignOutViewModel @Inject constructor(private val session: Session) : ViewModel(), KeysBackupStateListener { // Keys exported manually var keysExportedToFile = MutableLiveData() var keysBackupState = MutableLiveData() - private var mxSession: Session? = null + init { + session.getKeysBackupService().addListener(this) - fun init(session: Session) { - if (mxSession == null) { - mxSession = session - - mxSession?.getKeysBackupService() - ?.addListener(this) - } - - keysBackupState.value = mxSession?.getKeysBackupService() - ?.state + keysBackupState.value = session.getKeysBackupService().state } /** * Safe way to get the current KeysBackup version */ fun getCurrentBackupVersion(): String { - return mxSession - ?.getKeysBackupService() - ?.currentBackupVersion - ?: "" + return session.getKeysBackupService().currentBackupVersion ?: "" } /** * Safe way to get the number of keys to backup */ fun getNumberOfKeysToBackup(): Int { - return mxSession - ?.inboundGroupSessionsCount(false) - ?: 0 + return session.inboundGroupSessionsCount(false) } /** * Safe way to tell if there are more keys on the server */ fun canRestoreKeys(): Boolean { - return mxSession - ?.getKeysBackupService() - ?.canRestoreKeys() == true + return session.getKeysBackupService().canRestoreKeys() } override fun onCleared() { super.onCleared() - mxSession?.getKeysBackupService() - ?.removeListener(this) + session.getKeysBackupService().removeListener(this) } override fun onStateChange(newState: KeysBackupState) { @@ -84,7 +68,7 @@ class SignOutViewModel @Inject constructor() : ViewModel(), KeysBackupStateListe fun refreshRemoteStateIfNeeded() { if (keysBackupState.value == KeysBackupState.Disabled) { - mxSession?.getKeysBackupService()?.checkAndStartKeysBackup() + session.getKeysBackupService().checkAndStartKeysBackup() } } @@ -92,13 +76,9 @@ class SignOutViewModel @Inject constructor() : ViewModel(), KeysBackupStateListe /** * The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready */ - fun doYouNeedToBeDisplayed(session: Session?): Boolean { - return session - ?.inboundGroupSessionsCount(false) - ?: 0 > 0 - && session - ?.getKeysBackupService() - ?.state != KeysBackupState.ReadyToBackUp + fun doYouNeedToBeDisplayed(session: Session): Boolean { + return session.inboundGroupSessionsCount(false) > 0 + && session.getKeysBackupService().state != KeysBackupState.ReadyToBackUp } } } diff --git a/vector/src/main/res/drawable/ic_alert_triangle.xml b/vector/src/main/res/drawable/ic_alert_triangle.xml new file mode 100644 index 0000000000..0daaca920a --- /dev/null +++ b/vector/src/main/res/drawable/ic_alert_triangle.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_check_white_24dp.xml b/vector/src/main/res/drawable/ic_check_white_24dp.xml new file mode 100644 index 0000000000..0aedb18ddd --- /dev/null +++ b/vector/src/main/res/drawable/ic_check_white_24dp.xml @@ -0,0 +1,4 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_idea.xml b/vector/src/main/res/drawable/ic_idea.xml new file mode 100644 index 0000000000..4be057ec6c --- /dev/null +++ b/vector/src/main/res/drawable/ic_idea.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_room_actions_leave.xml b/vector/src/main/res/drawable/ic_room_actions_leave.xml new file mode 100644 index 0000000000..346defac7c --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_actions_leave.xml @@ -0,0 +1,47 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/ic_room_actions_notifications_all.xml b/vector/src/main/res/drawable/ic_room_actions_notifications_all.xml new file mode 100644 index 0000000000..5bf7226c59 --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_actions_notifications_all.xml @@ -0,0 +1,39 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_room_actions_notifications_all_noisy.xml b/vector/src/main/res/drawable/ic_room_actions_notifications_all_noisy.xml new file mode 100644 index 0000000000..0eec24a115 --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_actions_notifications_all_noisy.xml @@ -0,0 +1,39 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_room_actions_notifications_mentions.xml b/vector/src/main/res/drawable/ic_room_actions_notifications_mentions.xml new file mode 100644 index 0000000000..6bdf317097 --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_actions_notifications_mentions.xml @@ -0,0 +1,47 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/ic_room_actions_notifications_mutes.xml b/vector/src/main/res/drawable/ic_room_actions_notifications_mutes.xml new file mode 100644 index 0000000000..89bbc832cb --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_actions_notifications_mutes.xml @@ -0,0 +1,47 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/ic_room_actions_settings.xml b/vector/src/main/res/drawable/ic_room_actions_settings.xml new file mode 100644 index 0000000000..c80dfe0409 --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_actions_settings.xml @@ -0,0 +1,39 @@ + + + + + + diff --git a/vector/src/main/res/layout/activity.xml b/vector/src/main/res/layout/activity.xml index df0cd3fbea..b5203cd589 100644 --- a/vector/src/main/res/layout/activity.xml +++ b/vector/src/main/res/layout/activity.xml @@ -1,7 +1,6 @@ @@ -13,7 +12,7 @@ android:elevation="4dp" app:layout_constraintTop_toTopOf="parent" /> - - + + - - @@ -20,7 +20,7 @@ - - - diff --git a/vector/src/main/res/layout/activity_simple.xml b/vector/src/main/res/layout/activity_simple.xml index 3ffb112b60..8dc41487fe 100644 --- a/vector/src/main/res/layout/activity_simple.xml +++ b/vector/src/main/res/layout/activity_simple.xml @@ -1,5 +1,5 @@ - diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml index 3ce38c425d..4c9225dba7 100644 --- a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml +++ b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml @@ -90,10 +90,8 @@ - + android:layout_height="match_parent"> + + + + + + diff --git a/vector/src/main/res/layout/fragment_home_detail.xml b/vector/src/main/res/layout/fragment_home_detail.xml index bda23973ee..7c0886efc5 100644 --- a/vector/src/main/res/layout/fragment_home_detail.xml +++ b/vector/src/main/res/layout/fragment_home_detail.xml @@ -63,7 +63,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/syncStateView" /> - - - - @@ -30,7 +30,7 @@ android:layout_marginEnd="16dp" android:layout_marginRight="16dp" android:layout_marginBottom="16dp" - android:accessibilityTraversalBefore="@+id/roomListEpoxyRecyclerView" + android:accessibilityTraversalBefore="@+id/roomListView" android:contentDescription="@string/a11y_create_direct_message" android:scaleType="center" android:src="@drawable/ic_fab_add_chat" @@ -47,7 +47,7 @@ android:layout_marginEnd="16dp" android:layout_marginRight="16dp" android:layout_marginBottom="16dp" - android:accessibilityTraversalBefore="@+id/roomListEpoxyRecyclerView" + android:accessibilityTraversalBefore="@+id/roomListView" android:contentDescription="@string/a11y_create_room" android:src="@drawable/ic_fab_add_room" android:visibility="gone" diff --git a/vector/src/main/res/layout/item_bottom_sheet_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml index 131ee0e63c..0ad7a211da 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_action.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml @@ -1,52 +1,72 @@ - + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@+id/actionStartSpace" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/ic_room_actions_notifications_all" /> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/actionSelected" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@id/actionStartSpace" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_default="wrap" + tools:text="zbla azjazjaz s sdkqdskdsqk kqsdkdqsk kdqsksqdk" /> + - + + diff --git a/vector/src/main/res/layout/item_bottom_sheet_room_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_room_preview.xml new file mode 100644 index 0000000000..1a38feface --- /dev/null +++ b/vector/src/main/res/layout/item_bottom_sheet_room_preview.xml @@ -0,0 +1,59 @@ + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_emoji_result.xml b/vector/src/main/res/layout/item_emoji_result.xml new file mode 100644 index 0000000000..cc93ee0d5d --- /dev/null +++ b/vector/src/main/res/layout/item_emoji_result.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_help_footer.xml b/vector/src/main/res/layout/item_help_footer.xml new file mode 100644 index 0000000000..7a1db62872 --- /dev/null +++ b/vector/src/main/res/layout/item_help_footer.xml @@ -0,0 +1,37 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index fbe3b70551..50ed0aae23 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -78,6 +78,13 @@ android:layout="@layout/item_timeline_event_text_message_stub" tools:visibility="visible" /> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_user.xml b/vector/src/main/res/layout/item_user.xml new file mode 100644 index 0000000000..20e339528a --- /dev/null +++ b/vector/src/main/res/layout/item_user.xml @@ -0,0 +1,58 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/merge_overlay_waiting_view.xml b/vector/src/main/res/layout/merge_overlay_waiting_view.xml index 8f09ed0988..b7e7bf41c7 100644 --- a/vector/src/main/res/layout/merge_overlay_waiting_view.xml +++ b/vector/src/main/res/layout/merge_overlay_waiting_view.xml @@ -3,8 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> + android:layout_height="match_parent"> @@ -50,10 +52,12 @@ android:id="@+id/attachmentGalleryButton" style="@style/AttachmentTypeSelectorButton" android:src="@drawable/ic_attachment_gallery_white_24dp" + android:contentDescription="@string/attachment_type_gallery" tools:background="@color/colorAccent" /> @@ -70,10 +74,12 @@ android:id="@+id/attachmentFileButton" style="@style/AttachmentTypeSelectorButton" android:src="@drawable/ic_attachment_file_white_24dp" + android:contentDescription="@string/attachment_type_file" tools:background="@color/colorAccent" /> @@ -99,10 +105,12 @@ android:id="@+id/attachmentAudioButton" style="@style/AttachmentTypeSelectorButton" android:src="@drawable/ic_attachment_audio_white_24dp" + android:contentDescription="@string/attachment_type_audio" tools:background="@color/colorAccent" /> @@ -119,10 +127,12 @@ android:id="@+id/attachmentContactButton" style="@style/AttachmentTypeSelectorButton" android:src="@drawable/ic_attachment_contact_white_24dp" + android:contentDescription="@string/attachment_type_contact" tools:background="@color/colorAccent" /> @@ -139,14 +149,16 @@ android:id="@+id/attachmentStickersButton" style="@style/AttachmentTypeSelectorButton" android:src="@drawable/ic_attachment_stickers_white_24dp" + android:contentDescription="@string/attachment_type_sticker" tools:background="@color/colorAccent" /> - \ No newline at end of file + diff --git a/vector/src/main/res/menu/menu_emoji_reaction_picker.xml b/vector/src/main/res/menu/menu_emoji_reaction_picker.xml index 98242f57bb..87135d64ea 100644 --- a/vector/src/main/res/menu/menu_emoji_reaction_picker.xml +++ b/vector/src/main/res/menu/menu_emoji_reaction_picker.xml @@ -4,6 +4,7 @@ diff --git a/vector/src/main/res/values-ca/strings.xml b/vector/src/main/res/values-ca/strings.xml index d9af5deb46..9be9a0c077 100644 --- a/vector/src/main/res/values-ca/strings.xml +++ b/vector/src/main/res/values-ca/strings.xml @@ -1430,13 +1430,13 @@ Per què triar Riot.im? Ho sentim, els dispositius amb SO Android inferior a 5.0 no suporten trucades multi-usuari amb Jitsi No heu configurat cap administrador d\'integracions. - Un nou dispositiu està sol·licitant claus d\'encriptació. -\nNom del dispositiu: %1$s -\nVist per última vegada: %s$s + Un nou dispositiu està sol·licitant claus d\'encriptació. +\nNom del dispositiu: %1$s +\nVist per última vegada: %2$s \nSi no heu iniciat sessió en un altre dispositiu, ignoreu la sol·licitud. - Un dispositiu no verificat està sol·licitant claus d\'encriptació. -\nNom del dispositiu: %1$s -\nVist per última vegada: %s$s + Un dispositiu no verificat està sol·licitant claus d\'encriptació. +\nNom del dispositiu: %1$s +\nVist per última vegada: %2$s \nSi no heu iniciat sessió en un altre dispositiu, ignoreu la sol·licitud. Verificar diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index b8b86b5081..251c42c00c 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -343,7 +343,7 @@ Bist du sicher? Nachrichten konnten nicht gesendet werden. %1$s oder %2$s? Nachrichten wurden nicht gesendet, da unbekannte Geräte anwesend sind. %1$s oder %2$s? Alles erneut senden - Senden abbrechen + alles abbrechen Nicht gesendete Nachrichten erneut senden Nicht gesendete Nachrichten löschen Datei nicht gefunden @@ -445,7 +445,7 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.Hintergrundsynchronisierung Hintergrundsynchronisierung aktivieren Timeout für Synchronisierungsanfragen - Verzögerung zwischen jeder Anfrage + Verzögerung zwischen jeder Synchronisierung Sekunde Sekunden @@ -473,10 +473,10 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.Räume mit ungelesenen Benachrichtigungen anheften Räume mit ungelesenen Nachrichten anheften Geräte - Geräte-Details + Geräte Information ID - Name - Gerätename + Öffentlicher Name + Öffentlichen Namen aktualisieren Zuletzt gesehen %1$s @ %2$s Diese Operation benötigt weitere Authentifizierung. @@ -607,9 +607,9 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.Entschlüsselungsfehler Absendergeräteinformationen - Gerätename - Name - Geräte-ID + Öffentlicher Name + Öffentlicher Name + ID Geräte-Schlüssel Verifizierungsstatus Ed25519-Fingerabdruck @@ -1552,8 +1552,8 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A Überprüfen Sie dieses Gerät, um es als vertrauenswürdig zu markieren. Das Vertrauen auf Geräte von Partnern gibt Ihnen zusätzliche Sicherheit, wenn Sie verschlüsselte End-to-End-Nachrichten verwenden. Das Verifizieren dieses Benutzers wird seine Geräte als \"vertraut\" markieren und dein Gerät bei ihnen als \"vertraut\" markieren. - Verifizieren Sie diesen Benutzer, indem Sie bestätigen, dass folgendes Emoji auf dessen Bildschirm erscheint. - Verifizieren Sie diesen Benutzer, indem Sie bestätigen, dass die folgende Nummer auf dessen Bildschirm erscheint. + Überprüfen Sie dieses Gerät, indem Sie bestätigen, dass das folgende Emoji auf dem Bildschirm des Partners angezeigt wird + Überprüfen Sie dieses Gerät, indem Sie bestätigen, dass die folgenden Zahlen auf dem Bildschirm des Partners angezeigt werden Es ist nichts aufgetaucht\? Noch nicht alle Clients unterstützen die interaktive Verifikation. . Verwenden Sie die Alte-Überprüfung @@ -1656,4 +1656,56 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A app_id: Überprüfung + Keine + Widerrufen + Trennen + Kein Integrationsserver konfiguriert. + + Anruf aufgrund eines falsch konfigurierten Servers fehlgeschlagen + Versuchen Sie es mit %s + Nicht erneut fragen + + Richten Sie eine E-Mail für die Kontowiederherstellung ein, die später von Personen, die Sie kennen, optional gefunden werden kann. + Richten Sie ein Telefon ein und lassen Sie es später optional von Personen erkennen, die Sie kennen. + Legen Sie eine E-Mail für die Kontowiederherstellung fest. Verwenden Sie eine spätere E-Mail oder ein späteres Telefon, um von Personen, die Sie kennen, optional gefunden zu werden. + Legen Sie eine E-Mail für die Kontowiederherstellung fest. Verwenden Sie eine spätere E-Mail oder ein späteres Telefon, um von Personen, die Sie kennen, optional gefunden zu werden. + Fallback-Call-Assist-Server zulassen + Optimiert für die Batterie + Optimiert für die Echtzeit + Keine Hintergrundsynchronisation + Fehler beim Aktualisieren der Einstellungen. + + + Bevorzugtes Synchronisationsintervall + Discovery + Öffentlicher Name (sichtbar für Personen, mit denen Sie kommunizieren) + Der öffentliche Name eines Geräts ist für Personen sichtbar, mit denen Sie kommunizieren + Um fortzufahren, müssen Sie die Bedingungen dieses Dienstes akzeptieren. + + Sie verwenden keinen Identity Server + Es ist kein Identitätsserver konfiguriert. Sie müssen Ihr Kennwort zurücksetzen. + + Sie versuchen anscheinend, eine Verbindung zu einem anderen Homeserver herzustellen. Möchten Sie sich abmelden\? + + push_key: + app_display_name: + Url: + Nutzungsbedingungen + Nutzungsbedingungen überprüfen + Für andere auffindbar sein + Verwenden Sie Bots, Bridges, Widgets und Sticker-Packs + + Lesen Sie bei + + + Identitätsserver + Trennen Sie den Identitätsserver + Konfigurieren Sie den Identitätsserver + Identitätsserver ändern + Erkennbare E-Mail-Adressen + Erkennungsoptionen werden angezeigt, sobald Sie eine E-Mail hinzugefügt haben. + ausstehend + + Gib einen neuen Identitätsserver ein + Konnte keine Verbindung zum Heimserver herstellen. diff --git a/vector/src/main/res/values-es-rMX/strings.xml b/vector/src/main/res/values-es-rMX/strings.xml index 423ecb5438..313e7737d1 100644 --- a/vector/src/main/res/values-es-rMX/strings.xml +++ b/vector/src/main/res/values-es-rMX/strings.xml @@ -779,4 +779,9 @@ Dispositivos desconocidos: Enviar una respuesta cifrada… Enviar una respuesta (sin cifrar)… + Latn + + Iniciando servicio + Copia de seguridad de la clave + Usar copia de seguridad de la clave diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index 894f7e3d47..252acefabc 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -1420,8 +1420,8 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Firma autocompletar opciones del servidor - Riot ha detectado una configuración personalizada del servidor para el dominio de su ID de usuario \"%s\": -\n%s + Riot ha detectado una configuración personalizada del servidor para el dominio de su ID de usuario \"%1$s\": +\n%2$s Configuración de uso Origen predeterminado de medios diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml index 9e02bf8103..57eed05cca 100644 --- a/vector/src/main/res/values-eu/strings.xml +++ b/vector/src/main/res/values-eu/strings.xml @@ -1697,4 +1697,73 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada. Orain e-mail helbideak edo telefono zenbakiak partekatzen dituzu %1$s zerbitzarian. %2$s zerbitzarira konektatu beharko zara partekatzeari uzteko. Onartu %s identitate-zerbitzariaren erabilera baldintzak besteek zu e-mail helbidea edo telefonoa erabiliz aurkitzea ahalbidetzeko. + Latn + + Gaitu egunkari xehetsuak. + Amorruz astintzean egunkari xehetsuak bidaltzeak garatzaileei laguntzen diete. Gaituta badago ere, aplikazioak ez ditu mezuen edukiak edo beste datu probaturik gordetzen egunkarian. + + + Saiatu berriro zure hasiera-zerbitzariaren erabilera baldintzak onartu eta gero. + + Badirudi zerbitzariak luze hartu duela erantzuteko, hau konexio kaxkar baten ondorioz izan daiteke edo zerbitzarian errore bat dagoelako. Saiatu berriro geroago. + + Bidali eranskina + + Ireki nabigazio-tiradera + Ireki gela sortzeko menua + Itxi gela sortzeko menua… + Sortu elkarrizketa zuzen berria + Sortu gela berria + Itxi gakoen babes-kopiaren banda + Erakutsi pasahitza + Ezkutatu pasahitza + Jauzi behera + + %1$s, %2$s eta beste %3$d erabiltzailek irakurria + %1$s, %2$s eta%3$s erabiltzaileek irakurria + %1$s eta %2$s erabiltzaileek irakurria + %s erabiltzaileak irakurria + + Erabiltzaile batek irakurria + %d erabiltzailek irakurria + + + \'%1$s\' fitxategia (%2$s) handiegia da igotzeko. Muga %3$s da. + + Errore bat gertatu da eranskina eskuratzean. + Fitxategia + Kontaktua + Kamera + Audioa + Galeria + Eranskailua + Ezin izan dira partekatutako datuak kudeatu + + Spama da + Desegokia da + Salaketa pertsonalizatua + Salatu eduki hau + Eduki hau salatzeko arrazoia + SALATU + BLOKEATU ERABILTZAILEA + + Edukia salatuta + Eduki hau salatu da. +\n +\nEz baduzu erabiltzaile honen eduki gehiago ikusi nahi, bere mezuak ezkutatzeko blokeatu dezakezu + Spam gisa salatua + Eduki hau spam gisa salatu da. +\n +\nEz baduzu erabiltzaile honen eduki gehiago ikusi nahi, bere mezuak ezkutatzeko blokeatu dezakezu + Desegoki gisa salatua + Eduki hau desegoki gisa salatu da. +\n +\nEz baduzu erabiltzaile honen eduki gehiago ikusi nahi, bere mezuak ezkutatzeko blokeatu dezakezu + + Riot-ek zure E2E gakoak diskoan gordetzeko baimena behar du. +\n +\nBaimendu sarbidea hurrengo laster-leihoan zure gakoak eskuz esportatu ahal izateko. + + Ez dago sare konexiorik orain + diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 3b037a9ead..651978f92f 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -6,7 +6,7 @@ FI - Keskustelut + Viestit Huone Asetukset Jäsenen tiedot @@ -155,7 +155,7 @@ Toista salasana Vahvista uusi salasanasi Väärä käyttäjätunnus ja/tai salasana - Käyttäjätunnus saa koostua vain kirjaimista a-z, numeroista, pisteistä, väliviivoista ja alaviivoista + Käyttäjätunnus saa koostua vain kirjaimista a-z, numeroista, pisteistä, yhdysviivoista ja alaviivoista Liian lyhyt salasana (vähintään 6 merkkiä) Salasana puuttuu Tämä ei näytä oikealta sähköpostiosoitteelta @@ -175,15 +175,15 @@ Kotipalvelin: Identiteettipalvelin: Olen varmistanut sähköpostiosoitteeni - Nollataksesi salasanasi, anna tilisi sähköpostiosoite: - Anna tilisi sähköpostiosoite. + Palauttaaksesi salasanasi, anna tiliisi liitetty sähköpostiosoite: + Anna tiliisi liitetty sähköpostiosoite. Anna uusi salasana. Osoitteeseen %s on lähetetty sähköposti. Kun olet avannut siinä olevan linkin, paina alla olevaa nappia. Sähköpostiosoitteesi vahvistaminen epäonnistui. Varmista, että klikkasit sähköpostissa olevaa linkkiä Salasanasi on vaihdettu.\n\nSinut on kirjauduttu ulos kaikista laitteistasi, etkä enää saa viesti-ilmoituksia. Ottaaksesi käyttöön ilmoitukset uudelleen, kirjaudu sisään uudelleen kaikilla laitteillasi. - URL:n on alettava seuraavasti: http[s]:// + URL-osoitteen on alettava seuraavasti: http[s]:// Kirjautuminen epäonnistui: Verkkovirhe Kirjautuminen epäonnistui Rekisteröityminen epäonnistui: Verkkovirhe @@ -232,7 +232,7 @@ Yhdistetty Yhdistetään… - Puhelu loppu + Puhelu loppui Soitetaan… Saapuva puhelu Saapuva videopuhelu @@ -285,7 +285,7 @@ Hylkää - Hyppää ensimmäiseen lukemattomaan viestiin. + Siirry ensimmäiseen lukemattomaan viestiin. %s on kutsunut sinut huoneeseen @@ -298,7 +298,7 @@ Uusi keskustelu Lisää jäsen - 1 jäsen + yksi jäsen Poistu huoneesta @@ -349,7 +349,7 @@ Lähetä salattu viesti… Lähetä viesti (salaamaton)… Yhteys palvelimeen katkesi. - Viesteja ei lähetetty. %1$s vai %2$s\? + Viestejä ei lähetetty. %1$s vai %2$s\? Viestejä ei lähetetty koska huoneessa on tuntemattomia laitteita. %1$s vai %2$s\? Lähetä kaikki uudelleen Peruuta kaikki @@ -423,7 +423,7 @@ Unohda - Keskustelut + Viestit Asetukset Versio Käyttöehdot @@ -785,9 +785,9 @@ Yhteisöt Ei ryhmiä - Oletko varma, että haluat aloittaa uuden keskustelun käyttäjän %s kanssa\? - Oletko varma että haluat aloittaa äänipuhelun? - Oletko varma, että haluat aloittaa videopuhelun? + Haluatko varmasti aloittaa uuden keskustelun käyttäjän %s kanssa\? + Haluatko varmasti aloittaa äänipuhelun\? + Haluatko varmasti aloittaa videopuhelun\? Ryhmälistaus @@ -933,12 +933,12 @@ Haluatko lisätä paketteja? Avaa otsikko Synkronoidaan… - yksi aktiivinen käyttäjä - %d aktiivista käyttäjää + yksi aktiivinen jäsen + %d aktiivista jäsentä - yksi käyttäjä - %d käyttäjää + yksi jäsen + %d jäsentä 1 s @@ -1136,12 +1136,12 @@ Haluatko lisätä paketteja? Kirjoita tähän… - yksi lukematon viesti - %d lukematonta viestiä + yksi lukematon ilmoitettu viesti + %d lukematonta ilmoitettua viestiä - 1 lukematon viesti - %d lukematonta viestiä + yksi lukematon ilmoitettu viesti + %d lukematonta ilmoitettua viestiä yksi huone @@ -1156,7 +1156,7 @@ Haluatko lisätä paketteja? Parametri ei ole kelvollinen. Käynnistä järjestelmän kamera Riotin kameraruudun sijaan. Käytä näppäimistön rivinvaihtopainiketta viestin lähettämiseen - Tämä vaihtoehto vaatii kolmannen osapuolen sovelluksen viestien tallennukseen. + Tämä valinta vaatii kolmannen osapuolen sovelluksen viestien tallennukseen. Komento ”%s” vaatii enemmän parametreja, tai jotkin parametrit ovat virheellisiä. Näyttää toiminnon @@ -1190,17 +1190,17 @@ Haluatko lisätä paketteja? Jatkaaksesi kotipalvelimen %1$s käyttöä, sinun täytyy hyväksyä palvelun käyttöehdot. Näytä ehdot - Tämä tekee tunnuksestasi lopullisesti käyttökelvottoman. Et voi kirjautua sisään eikä kukaan pysty rekisteröimään tunnusta samalla käyttäjä-ID:llä. Tämä saa tunnuksesi lähtemään kaikista huoneista, joissa se on osallisena, ja se poistaa tunnuksen tiedot identiteettipalvelimelta. Tämä toiminto on peruuttamaton. + Tämä tekee tilistäsi lopullisesti käyttökelvottoman. Et voi kirjautua sisään eikä kukaan pysty rekisteröitymään samalla käyttäjätunnuksella. Tämä saa tilisi poistumaan kaikista huoneista, joissa se on osallisena, ja poistaa tilin tiedot identiteettipalvelimelta. Tämä toiminto on peruuttamaton. \n -\nTunnuksen otto pois käytöstä ei oletuksena saa meitä unohtamaan lähettämiäsi viestejä. Jos haluat meidän unohtavan viestisi, merkitse alapuolella oleva laatikko. +\nTilin deaktivointi ei oletuksena saa meitä unohtamaan lähettämiäsi viestejä. Jos haluat meidän unohtavan viestisi, merkitse alapuolella oleva valintaruutu. \n -\nViestin näkyvyys Matrixissa on samanlainen kuin sähköpostissa. Vaikka unohdamme viestisi, kaikki tahot, joilla viestisi jo on, tulevat pääsemään omaan kopioonsa viesteistäsi. - Unohda kaikki viestit, jotka olen lähittänyt, kun tunnuksesi on deaktivoitu (varoitus: tämä aiheuttaa tulevien käyttäjien näkevän vanhat keskustelusi epätäydellisinä) +\nViestien näkyvyys Matrixissa on samantapainen kuin sähköpostissa. Viestiesi unohtaminen tarkoittaa, että lähettämiäsi viestejä ei näytetä uusille tai rekisteröitymättömille käyttäjille. Ne rekisteröityneet käyttäjät, joilla viestisi jo on, pääsevät kuitenkin näkemään oman kopionsa niistä jatkossakin. + Unohda kaikki viestit, jotka olen lähettänyt, kun tilini on deaktivoitu (Varoitus: tästä seuraa, että tulevat käyttäjät näkevät vanhat keskustelut epätäydellisinä) Syötä käyttäjätunnus. Tämä huone on korvattu toisella huoneella Keskustelu jatkuu täällä Tämä huone on jatkoa toiselle keskustelulle - Täppää tästä nähdäksesi vanhat viestit + Paina tästä nähdäksesi vanhemmat viestit Resurssiraja saavutettu Ota yhteys ylläpitäjään @@ -1227,7 +1227,7 @@ Haluatko lisätä paketteja? Näytä infoalue Aina - Tiedotuksille ja virheille + Viesteille ja virheille Vain virheille %1$s: @@ -1249,7 +1249,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Poista salalause, jos haluat Riotin generoivan palautusavaimen. Matrix-istuntoa ei ole saatavilla - Älä ikinä menetä salattuja viestejä + Älä koskaan menetä salattuja viestejä Salatuissa huoneissa viestit ovat suojattuna osapuolten välisellä salauksella. Vain sinä ja vastaanottaja(t) omistavat avaimet näiden viestien lukemiseen. \n \nVarmuuskopioi avaimesi, jotta et menetä niitä. @@ -1258,9 +1258,9 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Tallenna avaimet käsin Turvaa varmuuskopiosi salalauseella. - Tallennamme salatun kopion avaimistasi kotipalvelimellesi. Suojaa varmuuskopiosi salalauseella pitääksesi sen turvattuna. -\n -\nTurvallisuuden takia tämän salalauseen tulisi olla eri tunnuksesi salasanasta. + Tallennamme salatun kopion avaimistasi kotipalvelimellesi. Suojaa varmuuskopiosi salalauseella pitääksesi sen turvattuna. +\n +\nParhaan turvallisuuden takaamiseksi salalauseen tulisi olla eri kuin tilisi salasana. Aseta salalause Luodaan varmuuskopiota Tai, turvaa varmuuskopio palautusavaimella, tallentamalla palautusavain johonkin turvalliseen paikkaan. @@ -1377,7 +1377,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Kirjaudu sisään kertakirjautumisella Tämä osoite ei ole saavutettavissa. Tarkistathan osoitteen - Laitteesi käyttää vanhentunutta, haavoittuvaista TLS-protokollan versiota. Turvallisuutesi vuoksi et pysty yhdistämään + Laitteesi käyttää vanhentunutta, haavoittuvaista TLS-protokollan versiota. Turvallisuutesi tähden et voi muodostaa yhteyttä Lähetä viesti enter-näppäimellä Näppäimistön enter-näppäin lähettää viestin sen sijaan, että se lisäisi rivinvaihdon @@ -1387,25 +1387,25 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös %1$s -> %2$s - Keskusteluohjelma, joka on täysin hallinnoitavissasi ja erittäin joustava. Riot sallii sinun kommunikoivan juuri sillä tavalla kuin haluat. [matrix]-sovellus — avoimen ja hajautetun kommunikaation standardi. + Keskustelusovellus, joka on sinun hallinnassasi ja erittäin joustava. Riot antaa sinun viestiä juuri sillä tavalla kuin haluat. Taustalla [matrix] – avoimen ja hajautetun viestinnän standardi. \n -\nKäytä ilmaista matrix.org-tunnusta, hanki oma palvelimesi osoitteesta https://modular.im tai käytä muuta Matrix-palvelinta. +\nHanki ilmainen matrix.org-tili, hanki oma palvelimesi osoitteesta https://modular.im tai käytä muuta Matrix-palvelinta. \n \nMiksi valita Riot.im\? \n -\n• Täydellinen kommunikaatio: rakenna huoneita tiimeillesi, ystävillesi ja yhteisöllesi — juuri niin kuin haluat! Keskustele, jaa tiedostoja, lisää sovelmia ja tee ääni‐ ja videopuheluita — kaikki tämä ilmaiseksi. +\n• Kattavat mahdollisuudet viestintään: rakenna huoneita tiimeillesi, ystävillesi ja yhteisöllesi – juuri niin kuin haluat! Keskustele, jaa tiedostoja, lisää sovelmia ja soita ääni‐ ja videopuheluita – kaikki tämä ilmaiseksi. +\n +\n• Tehokkaat integraatiot: Käytä Riotia tuntemiesi työkalujen kanssa. Riot.im mahdollistaa keskustelut jopa eri keskusteluohjelmia käyttävien ihmisten ja ryhmien kanssa. \n -\n• Tehokkaat integraatiot: Käytä Riotia juuri niillä työkaluilla, jotka tiedät ja joita rakastat. Riot.im jopa mahdollistaa keskustelun henkilöiden kanssa, jotka käyttävät eri keskusteluohjelmia. +\n• Yksityinen ja turvallinen: Pidä keskustelusi salaisina. Nykyaikainen osapuolten välinen salaus pitää huolen, että yksityiset keskustelut pysyvät yksityisinä. \n -\n• Yksityinen ja turvallinen: Pidä keskustelusi salaisina. Nykyaikainen osapuolten välinen salaus pitää huolen, että yksityiset keskustelut pysyvät yksityisenä. +\n• Avoin, ei suljettu: Avointa lähdekoodia ja rakennettu käyttämään Matrixia. Voit omistaa oman datasi ylläpitämällä omaa palvelintasi, tai käyttämällä palvelinta, johon luotat. \n -\n• Avoin, ei suljettu: Avointa koodia, ja rakennettu käyttämään Matrixia. Voit omistaa oman datasi ylläpitämällä omaa palvelintasi, tai käyttämällä palvelinta, johon luotat. -\n -\n• Kaikkialla, missä olet: pysy yhteydessä siellä, missä ikinä oletkin täysin synkronoidulla viestihistorialla kaikkien laitteidesi ja sivun https://riot.im välillä. +\n• Kaikkialla, missä olet: pysy yhteydessä missä ikinä oletkin, täysin synkronoidulla viestihistorialla kaikkien laitteidesi ja https://riot.im-verkkopalvelun välillä. - Uusi avainvarmuuskopio on löydetty. + Uusi avainvarmuuskopio löydetty. \n -\nJos et asettanut uutta palautustapaa, hyökkääjä saattaa yrittää päästä käsiksi tunnukseesi. Vaihda tunnuksesi salasana ja aseta uusi palautustapa asetuksissa välittömästi. +\nJos et asettanut uutta palautustapaa, hyökkääjä saattaa yrittää päästä käsiksi tiliisi. Vaihda tilisi salasana ja aseta uusi palautustapa asetuksissa välittömästi. Epäkelpo kotipalvelimen löytövastaus Automaattitäydennyksen palvelinasetukset Riot löysi mukautetun palvelinasetuksen userId:si domainille ”%1$s”: @@ -1646,7 +1646,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Katkaise yhteys identiteettipalvelimeen Määritä identiteettipalvelin Vaihda identiteettipalvelinta - Käytät palvelinta %1$s löytääksesi tuntemiasi ihmisiä ja jotta he löytäisivät sinut. + Käytät palvelinta %1$s löytääksesi tuntemiasi ihmisiä ja ollaksesi heidän löydettävissään. Et käytä tällä hetkellä identiteettipalvelinta. Jotta voit löytää tuntemiasi ihmisiä ja jotta he löytävät sinut, määritä identiteettipalvelin alla. Odottaa @@ -1657,4 +1657,54 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös Valitsemallasi identiteettipalvelimella ei ole käyttöehtoja. Jatka vain, jos luotat palvelun omistajaan Jaat sähköpostiosoitteita tai puhelinnumeroita identiteettipalvelimella %1$s. Sinun täytyy yhdistää uudelleen palvelimeen %2$s, jotta voit lopettaa niiden jakamisen. Hyväksy identiteettipalvelimen (%s) käyttöehdot salliaksesi, että sinut voi löytää sähköpostiosoitteen tai puhelinnumeron perusteella. + Aseta sähköposti tilin palauttamista varten. Myöhemmin voit halutessasi antaa ihmisten etsiä sinua sen perusteella. + Salli varalla oleva puhelun apupalvelin + Yhteyden katkaiseminen identiteettipalvelimeesi tarkoittaa, että muut käyttäjät eivät voi etsiä sinua etkä voi kutsua muita sähköpostin tai puhelinnumeron perusteella. + Lähetimme sinulle vahvistussähköpostin osoitteeseen %s, tarkista sähköpostisi ja klikkaa vahvistuslinkkiä + Ota yksityiskohtaiset lokit käyttöön. + Yritä uudelleen, kun olet hyväksynyt kotipalvelimesi käyttöehdot. + + Palvelimen vastaus näyttää viipyvän. Tämä voi johtua kehnosta yhteydestä tai palvelimillamme tapahtuneesta virheestä. Yritä hetken kuluttua uudelleen. + + Lähetä liite + + Luo uusi huone + Näytä salasana + Piilota salasana + Siirry loppuun + + %1$s, %2$s ja %3$d muuta lukivat + %1$s, %2$s ja %3$s lukivat + %1$s ja %2$s lukivat + %s luki + + 1 käyttäjä luki + %d käyttäjää luki + + + Liitettä noudettaessa tapahtui virhe. + Tiedosto + Kamera + Galleria + Tarra + Se on roskapostia + Se on sopimaton + Verkkoyhteyttä ei ole juuri nyt + + Ei mitään + Pyydä kotipalvelimesi (%1$s) ylläpitäjää määrittämään TURN-palvelin, jotta puhelut toimivat luotettavasti. +\n +\nVaihtoehtoisesti voit yrittää käyttää julkista palvelinta osoitteessa %2$s, mutta tämä ei ole yhtä luotettava vaihtoehto ja antaa IP-osoitteesi kyseisen palvelimen tietoon. Voit myös hallita tätä asetuksista. + Kokeile käyttää palvelinta %s + Käyttää palvelinta %s apupalvelimena, jos kotipalvelimesi ei tarjoa sellaista (IP-osoitteesi näkyy palvelimelle puhelun aikana) + Optimoitu akunkestoa varten + Riot synkronoi taustalla laitteen rajallisia resursseja (akkua) säästäen. +\nLaitteesi resurssien tilasta riippuen käyttöjärjestelmä saattaa lykätä synkronointia. + Optimoitu reaaliaikaa varten + Riot synkronoi taustalla täsmällisin aikavälein (säädettävä). +\nTämä vaikuttaa radion ja akun käyttöön. Näet pysyvän ilmoituksen, joka kertoo, että Riot kuuntelee tapahtumia. + Viestimuokkaukset + Ole löydettävissä + Tekstiviesti on lähetetty numeroon %s. Syötä sen sisältämä varmistuskoodi. + diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 1fe493d9d4..6057627163 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -783,7 +783,7 @@ Appareils inconnus : Abandonner le salon Badge - Secouer avec frustration pour signaler un bug + Secouer avec frustration pour signaler une anomalie Actions Synchronisation… @@ -1701,4 +1701,73 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Vous partagez actuellement des adresse e-mails et des numéros de téléphone sur le serveur d’identité %1$s. Vous devrez vous reconnecter à %2$s pour arrêter de les partager. Acceptez les conditions de service du serveur d’identité (%s) pour vous permettre d’être découvrable avec une adresse e-mail ou un numéro de téléphone. + Latn + + Activer les journaux verbeux. + Les journaux verbeux aideront les développeurs en fournissant plus de journaux quand vous envoyez un rapport d’anomalie. Même si cette option est activée, l’application n’envoie pas le contenu des messages ou toute autre donnée personnelle. + + + Réessayez quand vous aurez accepté les termes et conditions de votre serveur d’accueil. + + On dirait que le serveur mette trop de temps à répondre. Ça peut être dû à une mauvaise connexion ou à une erreur avec nos serveurs. Réessayez plus tard. + + Envoyer une pièce jointe + + Ouvrir le menu de navigation + Ouvrir le menu de création de salon + Fermer le menu de création de salon… + Créer une nouvelle conversation directe + Créer un nouveau salon + Fermer la bannière de sauvegarde des clés + Afficher le mot de passe + Masquer le mot de passe + Sauter en bas de page + + %1$s, %2$s et %3$d autres ont lu + %1$s, %2$s et %3$s ont lu + %1$s et %2$s ont lu + %s a lu + + 1 utilisateur a lu + %d utilisateurs ont lu + + + Le fichier « %1$s » (%2$s) est trop gros pour être envoyé. La limite est %3$s. + + Une erreur est survenue pendant la récupération de la pièce jointe. + Fichier + Contact + Appareil photo + Audio + Galerie + Sticker + Impossible de traiter les données de partage + + C’est du pourriel + C’est inapproprié + Signalement personnalisé + Signaler ce contenu + Motif de signalement de ce contenu + SIGNALER + BLOQUER L’UTILISATEUR + + Contenu signalé + Ce contenu a été signalé. +\n +\nSi vous ne voulez plus voir de contenu de cet utilisateur, vous pouvez le bloquer pour masquer ses messages + Signalé comme pourriel + Ce contenu a été signalé comme pourriel. +\n +\nSi vous ne voulez plus voir de contenu de cet utilisateur, vous pouvez le bloquer pour masquer ses messages + Signalé comme inapproprié + Ce contenu a été signalé comme inapproprié. +\n +\nSi vous ne voulez plus voir de contenu de cet utilisateur, vous pouvez le bloquer pour masquer ses messages + + Riot a besoin de votre permission pour sauvegarder vos clés de chiffrement sur le disque. +\n +\nAutorisez l’accès dans le prochaine fenêtre pour pouvoir exporter vos clés manuellement. + + Il n’y a aucune connexion au réseau pour le moment + diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 2e13eb7531..caca6bd7c8 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -1700,4 +1700,73 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró Az azonosítási szerverrel (%1$s) megosztod az e-mail címeket és telefonszámokat. Újra kell csatlakoznod ehhez: %2$s, hogy megállítsd a megosztást. Egyetértek az azonosítási szerver (%s) Felhasználási feltételeivel ahhoz, hogy megtalálható legyek e-mail címmel vagy telefonszámmal. + Latn + + Kibővített naplózás engedélyezése. + A kiterjesztett naplózás a fejlesztőknek nyújt több információt amikor hibajegyet küldesz. Még bekapcsolva sem naplóz üzenet tartalmat vagy más személyes adatot. + + + Kérlek ismételd meg miután elfogadtad a matrix szervered felhasználási feltételeit. + + Úgy tűnik a szerver sokáig nem válaszol, ennek a bizonytalan kapcsolat vagy egy hiba a szerverünkben lehet az oka. Kicsit később próbáld újra. + + Csatolmány küldése + + Navigációs panel megnyitása + Szoba készítés menü megnyitása + Szoba készítés menü bezárása… + Új közvetlen beszélgetés indítása + Új szoba készítése + Kulcs mentés csík bezárása + Jelszó mutatása + Jelszó elrejtése + Végére ugrás + + %1$s, %2$s és %3$d olvasták + %1$s, %2$s és %3$s olvasták + %1$s és %2$s olvasták + %s olvasta + + 1 felhasználó olvasta + %d felhasználó olvasta + + + \'%1$s\' (%2$s) fájl túl nagy a feltöltéshez. A korlát: %3$s. + + A csatolmány letöltésénél hiba történt. + Fájl + Kapcsolat + Kamera + Hang + Galéria + Matrica + Az adatmegosztást nem sikerül kezelni + + Ez nemkívánt (spam) + Ez nem idevaló + Egyedi jelentés + Tartalom bejelentése + A tartalom bejelentésének oka + JELENTÉS + FELHASZNÁLÓ BLOKKOLÁSA + + Tartalom bejelentve + Ez a tartalom bejelentve. +\n +\nHa nem akarsz ettől a felhasználótól több üzenetet látni akkor blokkolhatod, hogy az üzenetei ne jelenjenek meg számodra + Bejelentve nem kívántként (spam) + Ez a tartalom nem kívántnak (spam) lett bejelentve. +\n +\n Ha nem akarsz ettől a felhasználótól több üzenetet látni akkor blokkolhatod, hogy az üzenetei ne jelenjenek meg számodra + Nem idevalónak bejelentve + Ez a tartalom nem idevalónak lett bejelentve. +\n +\n Ha nem akarsz ettől a felhasználótól több üzenetet látni akkor blokkolhatod, hogy az üzenetei ne jelenjenek meg számodra + + Riotnak engedélyre van szüksége ahhoz, hogy a végponttól végpontig titkosító kulcsokat a lemezre menthesse. +\n +\nKérlek a következő felugró ablakban engedélyezd a hozzáférést, hogy a kulcsokat kézzel kimenthesd. + + Jelenleg nincs hálózati kapcsolat + diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index 1c879ad794..d46eaa987b 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -1745,4 +1745,73 @@ Per essere certo di non perdere nulla, mantieni gli aggiornamenti attivi."Attualmente stai condividendo indirizzi email o numeri di telefono sul server di identità %1$s. Dovrai riconnetterti a %2$s per fermarne la condivisione. Accetta le condizioni di servizio del server di identità (%s) per consentire di essere trovabile per email o numero di telefono. + Latn + + Attiva log dettagliati. + I log dettagliati aiuteranno gli sviluppatori fornendo loro più informazioni quando invii una segnalazione. Anche quando attivi, l\'applicazione non registra i contenuti dei messaggi o altri dati personali. + + + Riprova dopo avere accettato i termini e condizioni del tuo homeserver. + + Sembra che il server stia impiegando troppo tempo a rispondere, ciò può essere causato da scarsa connettività o un errore con i nostri server. Riprova fra qualche minuto. + + Invia allegato + + Apri il pannello di navigazione + Apri il menu di creazione stanza + Chiudi il menu di creazione stanza… + Crea una nuova conversazione diretta + Crea una nuova stanza + Chiudi il banner di backup chiavi + Mostra password + Nascondi password + Salta in fondo + + %1$s, %2$s ed altri %3$d hanno letto + %1$s, %2$s e %3$s hanno letto + %1$s e %2$s hanno letto + %s ha letto + + 1 utente ha letto + %d utenti hanno letto + + + Il file \'%1$s\' (%2$s) è troppo grande da inviare. Il limite è %3$s. + + Si è verificato un errore ricevendo l\'allegato. + File + Contatto + Fotocamera + Audio + Galleria + Adesivo + Errore di gestione dati condivisi + + È spam + È inappropriato + Segnalazione personalizzata + Segnala questo contenuto + Motivo della segnalazione + SEGNALA + BLOCCA UTENTE + + Contenuto segnalato + Questo contenuto è stato segnalato. +\n +\nSe non vuoi più vedere contenuti da questo utente, puoi bloccarlo per nascondere i suoi messaggi + Segnalato come spam + Questo contenuto è stato segnalato come spam. +\n +\nSe non vuoi più vedere contenuti da questo utente, puoi bloccarlo per nascondere i suoi messaggi + Segnalato come inappropriato + Questo contenuto è stato segnalato come inappropriato. +\n +\nSe non vuoi più vedere contenuti da questo utente, puoi bloccarlo per nascondere i suoi messaggi + + Riot richiede l\'autorizzazione per salvare le tue chiavi E2E su disco. +\n +\nPermetti l\'accesso nel prossimo pop-up per poter esportare le chiavi manualmente. + + Non c\'è nessuna connessione di rete al momento + diff --git a/vector/src/main/res/values-ko/strings.xml b/vector/src/main/res/values-ko/strings.xml index 7f76e47ea7..3f2469d766 100644 --- a/vector/src/main/res/values-ko/strings.xml +++ b/vector/src/main/res/values-ko/strings.xml @@ -302,8 +302,8 @@ 방 주제 전화 - 오는 전화에 Riot 기본 벨소리를 사용합니다 - 오는 전화 벨소리 + 수신 전화에 Riot 기본 벨소리를 사용합니다 + 수신 전화 벨소리 전화에 사용할 벨소리를 선택하세요: 전화 @@ -311,9 +311,9 @@ 전화 연결 중… 전화 종료됨 전화 중… - 오는 전화 - 오는 영상 통화 - 오는 음성 통화 + 수신 전화 + 수신 영상 통화 + 수신 음성 통화 전화 진행 중… 영상 통화 진행 중… @@ -412,8 +412,8 @@ 초대 이 방 떠나기 이 방에서 삭제하기 - 차단 - 차단 해제 + 출입 금지 + 출입 금지 풀기 추방 일반 사용자로 재 설정 중재자로 하기 @@ -432,7 +432,7 @@ 이 사용자들을 이 대화에서 추방하겠습니까\? - 이 사용자를 이 대화에서 차단하겠습니까\? + 이 사용자를 이 대화에서 출입 금지하겠습니까\? 이유 %s님을 이 대화에 초대하겠습니까\? @@ -720,7 +720,7 @@ 읽은 기록 보이기 세부적인 목록으로 읽은 목록을 클릭하세요. 참가 및 떠남 이벤트 보이기 - 초대, 추방, 그리고 차단은 영향이 없습니다. + 초대, 추방, 그리고 출입 금지은 영향이 없습니다. 계정 이벤트 보이기 아바타와 표시 이름 변경도 포함합니다. 사용자가 언급할 때 진동 @@ -854,7 +854,7 @@ 방의 링크를 아는 누구나, 손님 제외 방의 링크를 아는 누구나, 손님 포함 - 차단한 사용자 + 출입 금지한 사용자 고급 이 방의 내부 ID @@ -1064,8 +1064,8 @@ 인식할 수 없는 명령어: %s \"%s\" 명령어는 더 많은 매개 변수가 필요하거나, 일부 매개 변수가 옳지 않습니다. 활동 표시하기 - 주어진 ID로 사용자 차단하기 - 주어진 ID로 사용자 차단 풀기 + 주어진 ID로 사용자 출입 금지하기 + 주어진 ID로 사용자 출입 금지 풀기 사용자의 권한 등급 정의하기 주어진 ID로 사용자 강등하기 주어진 ID 현재 방에 사용자 초대하기 @@ -1114,7 +1114,7 @@ 커뮤니티 관리자가 이 커뮤니티에 대한 자세한 설명을 제공하지 않았습니다. %2$s님에 의해 %1$s 방에서 추방당했습니다 - %2$s님에 의해 %1$s 방에서 차단당했습니다 + %2$s님에 의해 %1$s 방에서 출입 금지당했습니다 이유: %1$s 다시 참가하기 방 잊어버리기 @@ -1584,7 +1584,7 @@ Riot은 (설정할 수 있는) 특정 시간에 주기적으로 백그라운드에거 동기화됩니다. \n이는 라디오와 배터리 사용에 영향을 주며 Riot이 이벤트를 수신하고 있는 상태라는 알림이 영구적으로 표시됩니다. 백그라운드 동기화 없음 - 앱이 백그라운드에 있을 때 오는 메시지의 알림을 받지 않습니다. + 앱이 백그라운드에 있을 때 수신 메시지의 알림을 받지 않습니다. 설정을 업데이트하는데 실패했습니다. @@ -1623,4 +1623,72 @@ 현재 이메일 주소나 전화번호를 ID 서버 %1$s와 공유하고 있습니다. 공유하기를 중지하려면 %2$s(으)로 다시 연결해야 합니다. ID 서버 (%s)의 서비스 약관에 동의하면 다른 사용자가 당신을 이메일 주소나 전화번호로 찾을 수 있게 됩니다. + Latn + + 상세 로그 켜기. + 상세 로그는 분노의 흔들기를 보낼 때 더 많은 로그를 제공해서 개발자에게 도움을 줍니다. 이 설정을 켜도 애플리케이션은 메시지 내용이나 다른 개인 정보를 기록하지 않습니다. + + + 홈서버의 이용 약관에 동의한 후 다시 시도해주세요. + + 서버의 응답 시간이 지연되고 있습니다. 연결 상태가 좋지 않거나 서버에서 오류가 발생했을지도 모릅니다. 나중에 다시 시도해주세요. + + 첨부 파일 보내기 + + 내비게이션 서랍 열기 + 방 만들기 메뉴 열기 + 방 만들기 메뉴 닫기… + 새 다이렉트 대화 만들기 + 새 방 만들기 + 키 백업 배너 닫기 + 비밀번호 보이기 + 비밀번호 감추기 + 맨 아래로 건너뛰기 + + %1$s님, %2$s님 외 %3$d명이 읽음 + %1$s님, %2$s님 그리고 %3$s님이 읽음 + %1$s님 그리고 %2$s님이 읽음 + %s님이 읽음 + + %d명이 읽음 + + + 파일 \'%1$s\' (%2$s)이(가) 업로드하기에 너무 큽니다. 제한은 %3$s입니다. + + 첨부 파일을 검색하는 중 오류가 발생했습니다. + 파일 + 연락처 + 카메라 + 소리 + 갤러리 + 스티커 + 공유 데이터를 처리할 수 없음 + + 스팸 문자입니다 + 부적절한 문자입니다 + 맞춤 신고 + 이 내용 신고하기 + 이 내용을 신고하는 이유 + 신고 + 사용자 출입 금지 + + 내용 신고됨 + 이 내용을 신고했습니다. +\n +\n이 사용자의 내용을 더 이상 보고 싶지 않다면, 사용자를 차단하거나 메시지를 감출 수 있습니다 + 스팸 문자로 신고됨 + 이 내용을 스팸 메일로 신고했습니다. +\n +\n이 사용자의 내용을 더 이상 보고 싶지 않다면, 사용자를 차단하거나 메시지를 감출 수 있습니다 + 부적절한 문자로 신고됨 + 이 내용을 부적절한 문자로 신고했습니다. +\n +\n이 사용자의 내용을 더 이상 보고 싶지 않다면, 사용자를 차단하거나 메시지를 감출 수 있습니다 + + Riot은 종단간 키를 디스크에 저장하려면 권한이 필요합니다. +\n +\n키를 수동으로 내보내려면 다음 팝업에서 접근을 허용해주세요. + + 현재 네트워크 연결이 없습니다 + diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index eaad9bf4aa..e815f01b9f 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -1792,4 +1792,9 @@ В настоящее время вы делитесь адресами электронной почты или телефонными номерами на сервере идентификации %1$s. Вам нужно повторно подключиться к %2$s, чтобы прекратить делиться ими. Примите Условия обслуживания сервера идентификации (%s), чтобы разрешить обнаружение по адресу электронной почты или номеру телефона. + Включить подробное журнал. + Отправить вложенное + + Откройте навигационный ящик + Откройте меню «Создать комнату» diff --git a/vector/src/main/res/values-sr/strings.xml b/vector/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000000..121589a6cd --- /dev/null +++ b/vector/src/main/res/values-sr/strings.xml @@ -0,0 +1,113 @@ + + + sr + RS + Cyrl + + Светла тема + Тамна тема + Црна тема + Status.im тема + + Иницијализација сервиса + Синхронизација у току… + Бучна обавештења + Тиха обавештења + + Поруке + Соба + Подешавања + Подаци о члану + Историјски + Пријава грешке + Пошаљи налепницу + Резервна копија кључева + Користи резервну копију кључева + Верификуј уређај + + Креирање резервне копије кључева се није завршило, молим сачекајте… + Изгубићете ваше шифроване поруке ако се сад одјавите + Креирање резервне копије кључева је у току. Ако се одјавите сад, изгубићете приступ вашим шифрованим порукама. + Сигурносна копија кључева би требало да буде активна на свим вашим уређајима како би избегли губитак приступа вашим шифрованим порукама. + Не желим моје шифроване поруке + Прављење резервне копије кључева у току… + Користи резервну копију кључева + Да ли сте сигурни\? + Изгубићете приступ вашим шифрованим порукама уколико не направите резервну копију кључева пре него што се одјавите. + + Учитавање… + + У реду + Откажи + Сачувај + Напусти + Остани + Пошаљи + Копирај + Пошаљи поново + Уклони + Подели + Прихвати + Прескочи + Готово + Обустави + Игнориши + Прегледај + Одбаци + + Изађи + Акције + Одјави се + Да ли сте сигурни да желите да се одјавите\? + Гласовни позив + Видео позив + Глобална претрага + Означи све као прочитано + Брзи одговор + Означи као прочитано + Отвори + Затвори + Онемогући + + Потврда + Упозорење + Грешка + + Омиљено + Људи + Собе + Позивнице + Низак приоритет + Разговори + Локални адресар + Листа корисника + Само Matrix контакти + Нема резултата + Нема подешених сервера идентитета. + + Собе + Листа соба + Нема соба + Пошаљи позивницу + Опишите ваш проблем овде + Прочитај + + Придружи се соби + Корисничко име + Направи налог + Пријави се + Одјави се + Пошаљи налепницу + Направи фотографију или видео снимак + Направи фотографију + Направи видео снимак + + Пријави се + Пријави се помоћу single sign-on + Направи налог + Прескочи + Адреса електронске поште или корисничко име + Лозинка + Нова лозинка + Корисничко име + diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index b54c05b784..a1d6ceb32b 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -1653,4 +1653,71 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 您目前正在身份識別伺服器 %1$s 上分享電子郵件地址或電話號碼。您將必須重新連線到 %2$s 以停止分享它們。 同意身份識別伺服器 (%s) 的服務條款以允許您被透過電子郵件地址或電話號碼探索。 + + 啟用詳細紀錄。 + 詳細紀錄可以協助開發者在您傳送憤怒搖晃時取得更多紀錄。即使啟用這個設定,應用程式依然不會紀錄訊息內容或任何個人資料。 + + + 請在您接受您家伺服器的條款與條件前繼續重試。 + + 看起來伺服器回應時間似乎太久了,這可能是不良的網路連線或我們的伺服器錯誤所造成。請稍後再試。 + + 傳送附件 + + 開啟導航選單 + 開啟建立聊天室選單 + 關閉建立聊天室選單…… + 建立新的直接對話 + 建立新的聊天室 + 關閉金鑰備份橫幅 + 顯示密碼 + 隱藏密碼 + 跳到底部 + + %1$s、%2$s 與 %3$d 個其他人已閱讀 + %1$s、%2$s 與 %3$s 已閱讀 + %1$s 與 %2$s 已閱讀 + %s 已閱讀 + + %d 個使用者已閱讀 + + + 檔案「%1$s」(%2$s) 太大無法上傳。限制為 %3$s。 + + 在擷取附件時遇到錯誤。 + 檔案 + 聯絡人 + 相機 + 音訊 + 相簿 + 貼圖 + 無法處理分享資料 + + 垃圾訊息 + 不合適 + 自訂回報 + 回報此內容 + 回報此內容的理由 + 回報 + 封鎖使用者 + + 內容已回報 + 此內容已回報。 +\n +\n如果您不想要看到從此使用者而來的更多內容,您可以封鎖他以隱藏他的訊息 + 回報為垃圾訊息 + 此內容已被回報為垃圾訊息。 +\n +\n如果您不想要看到從此使用者而來的更多內容,您可以封鎖他以隱藏他的訊息 + 回報為不合適 + 此內容已被回報為不合適。 +\n +\n如果您不想要看到從此使用者而來的更多內容,您可以封鎖他以隱藏他的訊息 + + Riot 需要權限以在磁碟上儲存您的 E2E 金鑰。 +\n +\n請在下個彈出視窗中允許存取以讓您可以手動匯出您的金鑰。 + + 目前沒有網路連線 + diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index e9a4296add..c30a1d99d9 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -34,6 +34,7 @@ + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 71fbc22acf..8d8be693e1 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -2,6 +2,26 @@ + Block user + + "All messages (noisy)" + "All messages" + "Mentions only" + "Mute" + "Settings" + "Leave the room" + "%1$s made no changes" + Sends the given message as a spoiler + Spoiler + Type keywords to find a reaction. + + You are not ignoring any users + + Long click on a room to see more options + + + %1$s made the room public to whoever knows the link. + %1$s made the room invite only. diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index f09cb0c874..f61a89482a 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -101,6 +101,7 @@ #CCC3C3C3 @color/accent_color_dark @android:color/black + #FFFFFFFF #565656 diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 1da010b8ff..aa343a11fc 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -101,6 +101,7 @@ #333C3C3C @color/accent_color_light #FFEEEEEE + #FF000000 #FFF2F2F2 diff --git a/vector/src/main/res/xml/vector_settings_ignored_users.xml b/vector/src/main/res/xml/vector_settings_ignored_users.xml deleted file mode 100644 index 4ad8aed5b6..0000000000 --- a/vector/src/main/res/xml/vector_settings_ignored_users.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 11209b5345..96471cfebe 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -39,7 +39,7 @@ android:title="@string/settings_send_typing_notifs" /> diff --git a/vector/src/main/res/xml/vector_settings_root.xml b/vector/src/main/res/xml/vector_settings_root.xml index e0cb4f778b..894784767a 100644 --- a/vector/src/main/res/xml/vector_settings_root.xml +++ b/vector/src/main/res/xml/vector_settings_root.xml @@ -37,10 +37,9 @@ + app:fragment="im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment" />