diff --git a/CHANGES.md b/CHANGES.md index 588a244a06..52b410e2b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,42 @@ +Changes in RiotX 0.22.0 (2020-XX-XX) +=================================================== + +Features ✨: + - Integration Manager and Widget support (#48) + - Send stickers (#51) + +Improvements 🙌: + - New wording for notice when current user is the sender + - Hide "X made no changes" event by default in timeline (#1430) + - Hide left rooms in breadcrumbs (#766) + - Handle PowerLevel properly (#627) + - Correctly handle SSO login redirection + - SSO login is now performed in the default browser, or in Chrome Custom tab if available (#1400) + - Improve checking of homeserver version support (#1442) + - Add capability to add and remove a room from the favorites (#1217) + +Bugfix 🐛: + - Switch theme is not fully taken into account without restarting the app + - Temporary fix to show error when user is creating an account on matrix.org with userId containing only digits (#1410) + - Reply composer overlay stays on screen too long after send (#1169) + - Fix navigation bar icon contrast on API in [21,27[ (#1342) + - Fix status bar icon contrast on API in [21,23[ + - Wrong /query request (#1444) + - Make Credentials.homeServer optional because it is deprecated (#1443) + +Translations 🗣: + - + +SDK API changes ⚠️: + - + +Build 🧱: + - + +Other changes: + - Send plain text in the body of events containing formatted body, as per https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes + - Update link to Modular url from "https://modular.im/" to "https://modular.im/services/matrix-hosting-riot" and open it using ChromeCustomTab + Changes in RiotX 0.21.0 (2020-05-28) =================================================== diff --git a/docs/integration_tests.md b/docs/integration_tests.md index f7557a87a2..0fa1998499 100644 --- a/docs/integration_tests.md +++ b/docs/integration_tests.md @@ -59,6 +59,12 @@ It's recommended to run tests using an Android Emulator and not a real device. F You can run all the tests in the `androidTest` folders. +It can be done using this command: + +```bash +./gradlew vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest +``` + ## Stop Synapse To stop Synapse, you can run the following commands: diff --git a/docs/signin.md b/docs/signin.md index e7368137ae..7d3a4f819b 100644 --- a/docs/signin.md +++ b/docs/signin.md @@ -2,7 +2,7 @@ This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver. -## Sign up flows +## Sign in flows ### Get the flow @@ -58,7 +58,7 @@ We get credential (200) ```json { "user_id": "@alice:matrix.org", - "access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg", + "access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lr", "home_server": "matrix.org", "device_id": "GTVREDALBF", "well_known": { @@ -117,7 +117,7 @@ We get the credentials (200) ```json { "user_id": "@alice:matrix.org", - "access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg", + "access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3Jnfrfdegfszsefddvf", "home_server": "matrix.org", "device_id": "WBSREDASND", "well_known": { @@ -145,12 +145,59 @@ Not supported yet in RiotX "flows": [ { "type": "m.login.sso" + }, + { + "type": "m.login.token" } ] } ``` -In this case, the user can click on "Sign in with SSO" and the web screen will be displayed on the page `https://homeserver.with.sso/_matrix/static/client/login/` and the credentials will be passed back to the native code through the JS bridge +In this case, the user can click on "Sign in with SSO" and the native web browser, or a ChromeCustomTab if the device supports it, will be launched on the page + +> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=riotx%3A%2F%2Friotx + +The parameter `redirectUrl` is set to `riotx://riotx`. + +ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs) + +The browser will then take care of the SSO login, which may include creating a third party account, entering an email, settings a display name, or any other possibilities. + +During the process, user may be asked to validate an email by clicking on a link it contains. The link has to be opened in the browser which initiates the authentication. This is why we cannot use WebView anymore. + +Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken` + +> riotx://riotx?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy + +This navigation is intercepted by RiotX by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token + +> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login' + +```json +{ + "type": "m.login.token", + "token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy" +} +``` + +We get the credentials (200) + +```json +{ + "user_id": "@alice:homeserver.with.sso", + "access_token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVyIGtleQowMDEwY2lkIGdlbiA9IDEKMDAyY2NpZCB1c2", + "home_server": "homeserver.with.sso", + "device_id": "DETBTVAHCH", + "well_known": { + "m.homeserver": { + "base_url": "https:\/\/homeserver.with.sso\/" + }, + "m.identity_server": { + "base_url": "https:\/\/vector.im" + } + } +} +``` ## Reset password 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 469bc514e0..5cc9d1fc00 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 @@ -16,6 +16,7 @@ package im.vector.matrix.rx +import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams @@ -61,7 +62,7 @@ class RxRoom(private val room: Room) { } } - fun liveStateEvent(eventType: String, stateKey: String): Observable> { + fun liveStateEvent(eventType: String, stateKey: QueryStringValue): Observable> { return room.getStateEventLive(eventType, stateKey).asObservable() .startWithCallable { room.getStateEvent(eventType, stateKey).toOptional() 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 a60e83ec96..e8fef1361d 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 @@ -17,6 +17,7 @@ package im.vector.matrix.rx import androidx.paging.PagedList +import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams @@ -28,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional @@ -54,10 +56,10 @@ class RxSession(private val session: Session) { } } - fun liveBreadcrumbs(): Observable> { - return session.getBreadcrumbsLive().asObservable() + fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable> { + return session.getBreadcrumbsLive(queryParams).asObservable() .startWithCallable { - session.getBreadcrumbs() + session.getBreadcrumbs(queryParams) } } @@ -151,6 +153,18 @@ class RxSession(private val session: Session) { session.getAccountDataEvents(types) } } + + fun liveRoomWidgets( + roomId: String, + widgetId: QueryStringValue, + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): Observable> { + return session.widgetService().getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes).asObservable() + .startWithCallable { + session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes) + } + } } fun Session.rx(): RxSession { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/session/room/send/MarkdownParserTest.kt new file mode 100644 index 0000000000..0dd2261e24 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/session/room/send/MarkdownParserTest.kt @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.send + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.commonmark.renderer.text.TextContentRenderer +import org.junit.Assert.assertEquals +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +/** + * It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild, + * we can add more tests to cover the edge cases. + * Some tests are suffixed with `_not_passing`, maybe one day we will fix them... + * Riot-Web should be used as a reference for expected results, but not always. Especially Riot-Web add lots of `\n` in the + * formatted body, which is quite useless. + * Also Riot-Web does not provide plain text body when formatted text is provided. The body contains what the user has entered. + * See https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes + */ +@Suppress("SpellCheckingInspection") +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class MarkdownParserTest : InstrumentedTest { + + /** + * Create the same parser than in the RoomModule + */ + private val markdownParser = MarkdownParser( + Parser.builder().build(), + HtmlRenderer.builder().build(), + TextContentRenderer.builder().build() + ) + + @Test + fun parseNoMarkdown() { + testIdentity("") + testIdentity("a") + testIdentity("1") + testIdentity("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " + + "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" + + "modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pari" + + "atur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + } + + @Test + fun parseSpaces() { + testIdentity(" ") + testIdentity(" ") + testIdentity("\n") + } + + @Test + fun parseNewLines() { + testIdentity("line1\nline2") + testIdentity("line1\nline2\nline3") + } + + @Test + fun parseBold() { + testType( + name = "bold", + markdownPattern = "**", + htmlExpectedTag = "strong" + ) + } + + @Test + fun parseItalic() { + testType( + name = "italic", + markdownPattern = "*", + htmlExpectedTag = "em" + ) + } + + @Test + fun parseItalic2() { + // Riot-Web format + "_italic_".let { markdownParser.parse(it) }.expect("italic", "italic") + } + + /** + * Note: the test is not passing, it does not work on Riot-Web neither + */ + @Test + fun parseStrike_not_passing() { + testType( + name = "strike", + markdownPattern = "~~", + htmlExpectedTag = "del" + ) + } + + @Test + fun parseCode() { + testType( + name = "code", + markdownPattern = "`", + htmlExpectedTag = "code", + plainTextPrefix = "\"", + plainTextSuffix = "\"" + ) + } + + @Test + fun parseCode2() { + testType( + name = "code", + markdownPattern = "``", + htmlExpectedTag = "code", + plainTextPrefix = "\"", + plainTextSuffix = "\"" + ) + } + + @Test + fun parseCode3() { + testType( + name = "code", + markdownPattern = "```", + htmlExpectedTag = "code", + plainTextPrefix = "\"", + plainTextSuffix = "\"" + ) + } + + @Test + fun parseUnorderedList() { + "- item1".let { markdownParser.parse(it).expect(it, "
  • item1
") } + "- item1\n- item2".let { markdownParser.parse(it).expect(it, "
  • item1
  • item2
") } + } + + @Test + fun parseOrderedList() { + "1. item1".let { markdownParser.parse(it).expect(it, "
  1. item1
") } + "1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "
  1. item1
  2. item2
") } + } + + @Test + fun parseHorizontalLine() { + "---".let { markdownParser.parse(it) }.expect("***", "
") + } + + @Test + fun parseH2AndContent() { + "a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "

a

b

") + } + + @Test + fun parseQuote() { + "> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "

quoted

") + } + + @Test + fun parseQuote_not_passing() { + "> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "

quoted
line2

") + } + + @Test + fun parseBoldItalic() { + "*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "italic bold") + "**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "bold italic") + } + + @Test + fun parseHead() { + "# head1".let { markdownParser.parse(it) }.expect("head1", "

head1

") + "## head2".let { markdownParser.parse(it) }.expect("head2", "

head2

") + "### head3".let { markdownParser.parse(it) }.expect("head3", "

head3

") + "#### head4".let { markdownParser.parse(it) }.expect("head4", "

head4

") + "##### head5".let { markdownParser.parse(it) }.expect("head5", "
head5
") + "###### head6".let { markdownParser.parse(it) }.expect("head6", "
head6
") + } + + @Test + fun parseHeads() { + "# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "

head1

head2

") + } + + @Test + fun parseBoldNewLines_not_passing() { + "**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "bold
line2") + } + + @Test + fun parseLinks() { + "[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """link""") + } + + @Test + fun parseParagraph() { + "# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "

head

content

") + } + + private fun testIdentity(text: String) { + markdownParser.parse(text).expect(text, null) + } + + private fun testType(name: String, + markdownPattern: String, + htmlExpectedTag: String, + plainTextPrefix: String = "", + plainTextSuffix: String = "") { + // Test simple case + "$markdownPattern$name$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix", + expectedFormattedText = "<$htmlExpectedTag>$name") + + // Test twice the same tag + "$markdownPattern$name$markdownPattern and $markdownPattern$name bis$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix and $plainTextPrefix$name bis$plainTextSuffix", + expectedFormattedText = "<$htmlExpectedTag>$name and <$htmlExpectedTag>$name bis") + + val textBefore = "a" + val textAfter = "b" + + // With sticked text before + "$textBefore$markdownPattern$name$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix", + expectedFormattedText = "$textBefore<$htmlExpectedTag>$name") + + // With text before and space + "$textBefore $markdownPattern$name$markdownPattern" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix", + expectedFormattedText = "$textBefore <$htmlExpectedTag>$name") + + // With sticked text after + "$markdownPattern$name$markdownPattern$textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter", + expectedFormattedText = "<$htmlExpectedTag>$name$textAfter") + + // With space and text after + "$markdownPattern$name$markdownPattern $textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter", + expectedFormattedText = "<$htmlExpectedTag>$name $textAfter") + + // With sticked text before and text after + "$textBefore$markdownPattern$name$markdownPattern$textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix$textAfter", + expectedFormattedText = "a<$htmlExpectedTag>$name$textAfter") + + // With text before and after, with spaces + "$textBefore $markdownPattern$name$markdownPattern $textAfter" + .let { markdownParser.parse(it) } + .expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix $textAfter", + expectedFormattedText = "$textBefore <$htmlExpectedTag>$name $textAfter") + } + + private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) { + assertEquals("TextContent are not identical", TextContent(expectedText, expectedFormattedText), this) + } +} diff --git a/matrix-sdk-android/src/main/assets/postMessageAPI.js b/matrix-sdk-android/src/main/assets/postMessageAPI.js new file mode 100755 index 0000000000..4936a78538 --- /dev/null +++ b/matrix-sdk-android/src/main/assets/postMessageAPI.js @@ -0,0 +1,54 @@ +var android_widget_events = {}; + +var sendObjectMessageToRiotAndroid = function(parameters) { + Android.onWidgetEvent(JSON.stringify(parameters)); +}; + +var onWidgetMessageToRiotAndroid = function(event) { + /* Use an internal "_id" field for matching onMessage events and requests + _id was originally used by the Modular API. Keep it */ + if (!event.data._id) { + /* The Matrix Widget API v2 spec says: + "The requestId field should be unique and included in all requests" */ + event.data._id = event.data.requestId; + } + /* Make sure to have one id */ + if (!event.data._id) { + event.data._id = Date.now() + "-" + Math.random().toString(36); + } + + console.log("onWidgetMessageToRiotAndroid " + event.data._id); + + if (android_widget_events[event.data._id]) { + console.log("onWidgetMessageToRiotAndroid : already managed"); + return; + } + + if (!event.origin) { + event.origin = event.originalEvent.origin; + } + + android_widget_events[event.data._id] = event; + + console.log("onWidgetMessageToRiotAndroid : manage " + event.data); + sendObjectMessageToRiotAndroid({'event.data': event.data}); +}; + +var sendResponseFromRiotAndroid = function(eventId, res) { + var event = android_widget_events[eventId]; + + console.log("sendResponseFromRiotAndroid to " + event.data.action + " for "+ eventId + ": " + JSON.stringify(res)); + + var data = JSON.parse(JSON.stringify(event.data)); + + data.response = res; + + console.log("sendResponseFromRiotAndroid ---> " + data); + + event.source.postMessage(data, event.origin); + android_widget_events[eventId] = true; + + console.log("sendResponseFromRiotAndroid to done"); +}; + +window.addEventListener('message', onWidgetMessageToRiotAndroid, false); diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt index d7c62f8bef..e7c24fadc8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt @@ -22,6 +22,15 @@ import java.net.Proxy data class MatrixConfiguration( val applicationFlavor: String = "Default-application-flavor", val cryptoConfig: MXCryptoConfig = MXCryptoConfig(), + val integrationUIUrl: String = "https://scalar.vector.im/", + val integrationRestUrl: String = "https://scalar.vector.im/api", + val integrationWidgetUrls: List = listOf( + "https://scalar.vector.im/_matrix/integrations/v1", + "https://scalar.vector.im/api", + "https://scalar-staging.vector.im/_matrix/integrations/v1", + "https://scalar-staging.vector.im/api", + "https://scalar-staging.riot.im/scalar/api" + ), /** * Optional proxy to connect to the matrix servers * You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.kt index af6e2277f4..f4d6d2505e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Constants.kt @@ -32,6 +32,6 @@ const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" * Path to use when the client want to connect using SSO * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login */ -const val SSO_FALLBACK_PATH = "/_matrix/client/r0/login/sso/redirect" +const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" const val SSO_REDIRECT_URL_PARAM = "redirectUrl" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt index d88cd5e74d..d50ed2f46e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt @@ -45,7 +45,7 @@ data class Credentials( * @Deprecated. Clients should extract the server_name from user_id (by splitting at the first colon) * if they require it. Note also that homeserver is not spelt this way. */ - @Json(name = "home_server") val homeServer: String, + @Json(name = "home_server") val homeServer: String?, /** * ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified. */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt index 853ea93544..36b812ce16 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt @@ -20,6 +20,7 @@ import android.net.Uri import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig.Builder import im.vector.matrix.android.internal.network.ssl.Fingerprint +import im.vector.matrix.android.internal.util.ensureTrailingSlash import okhttp3.CipherSuite import okhttp3.TlsVersion @@ -71,15 +72,11 @@ data class HomeServerConnectionConfig( throw RuntimeException("Invalid home server URI: " + hsUri) } // ensure trailing / - homeServerUri = if (!hsUri.toString().endsWith("/")) { - try { - val url = hsUri.toString() - Uri.parse("$url/") - } catch (e: Exception) { - throw RuntimeException("Invalid home server URI: $hsUri") - } - } else { - hsUri + val hsString = hsUri.toString().ensureTrailingSlash() + homeServerUri = try { + Uri.parse(hsString) + } catch (e: Exception) { + throw RuntimeException("Invalid home server URI: $hsUri") } return this } @@ -97,15 +94,11 @@ data class HomeServerConnectionConfig( throw RuntimeException("Invalid identity server URI: $identityServerUri") } // ensure trailing / - if (!identityServerUri.toString().endsWith("/")) { - try { - val url = identityServerUri.toString() - this.identityServerUri = Uri.parse("$url/") - } catch (e: Exception) { - throw RuntimeException("Invalid identity server URI: $identityServerUri") - } - } else { - this.identityServerUri = identityServerUri + val isString = identityServerUri.toString().ensureTrailingSlash() + this.identityServerUri = try { + Uri.parse(isString) + } catch (e: Exception) { + throw RuntimeException("Invalid identity server URI: $identityServerUri") } return this } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt index dd0c93a41c..3e824eeaee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt @@ -16,12 +16,10 @@ package im.vector.matrix.android.api.auth.data -import im.vector.matrix.android.internal.auth.data.LoginFlowResponse - -// Either a LoginFlowResponse, or an error if the homeserver is outdated +// Either a list of supported login types, or an error if the homeserver is outdated sealed class LoginFlowResult { data class Success( - val loginFlowResponse: LoginFlowResponse, + val supportedLoginTypes: List, val isLoginAndRegistrationSupported: Boolean, val homeServerUrl: String ) : LoginFlowResult() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowTypes.kt similarity index 95% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowTypes.kt index 4ff29d594a..922a4cca18 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowTypes.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowTypes.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.auth.data +package im.vector.matrix.android.api.auth.data object LoginFlowTypes { const val PASSWORD = "m.login.password" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt index 9dd1fa2012..93067d8ebb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt @@ -54,30 +54,4 @@ data class WellKnown( @Json(name = "m.integrations") val integrations: JsonDict? = null -) { - /** - * Returns the list of integration managers proposed - */ - fun getIntegrationManagers(): List { - val managers = ArrayList() - integrations?.get("managers")?.let { - (it as? ArrayList<*>)?.let { configs -> - configs.forEach { config -> - (config as? Map<*, *>)?.let { map -> - val apiUrl = map["api_url"] as? String - val uiUrl = map["ui_url"] as? String ?: apiUrl - if (apiUrl != null - && apiUrl.startsWith("https://") - && uiUrl!!.startsWith("https://")) { - managers.add(WellKnownManagerConfig( - apiUrl = apiUrl, - uiUrl = uiUrl - )) - } - } - } - } - } - return managers - } -} +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt index d7b2f5d960..9c296d5ddb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/login/LoginWizard.kt @@ -34,6 +34,12 @@ interface LoginWizard { deviceName: String, callback: MatrixCallback): Cancelable + /** + * Exchange a login token to an access token + */ + fun loginWithToken(loginToken: String, + callback: MatrixCallback): Cancelable + /** * Reset user password */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt index c3f4864232..ccc5908911 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/registration/Stage.kt @@ -21,15 +21,12 @@ sealed class Stage(open val mandatory: Boolean) { // m.login.recaptcha data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory) - // m.login.oauth2 // m.login.email.identity data class Email(override val mandatory: Boolean) : Stage(mandatory) // m.login.msisdn data class Msisdn(override val mandatory: Boolean) : Stage(mandatory) - // m.login.token - // m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username // and a password, the dummy stage has to be done data class Dummy(override val mandatory: Boolean) : Stage(mandatory) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/SenderNotificationPermissionCondition.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/SenderNotificationPermissionCondition.kt index cc6fe3f6ef..5d16825056 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/SenderNotificationPermissionCondition.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/SenderNotificationPermissionCondition.kt @@ -39,6 +39,6 @@ class SenderNotificationPermissionCondition( fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean { val powerLevelsHelper = PowerLevelsHelper(powerLevels) - return event.senderId != null && powerLevelsHelper.getUserPowerLevel(event.senderId) >= powerLevelsHelper.notificationLevel(key) + return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevelsHelper.notificationLevel(key) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt index 5d3e76f1d3..ef99133ba6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt @@ -25,8 +25,8 @@ sealed class QueryStringValue { object IsNotNull : QueryStringValue() object IsEmpty : QueryStringValue() object IsNotEmpty : QueryStringValue() - data class Equals(val string: String, val case: Case) : QueryStringValue() - data class Contains(val string: String, val case: Case) : QueryStringValue() + data class Equals(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue() + data class Contains(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue() enum class Case { SENSITIVE, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index c86ca25faf..5736e78a30 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.identity.IdentityService +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService @@ -42,6 +43,7 @@ import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.user.UserService +import im.vector.matrix.android.api.session.widgets.WidgetService /** * This interface defines interactions with a session. @@ -153,6 +155,16 @@ interface Session : */ fun identityService(): IdentityService + /** + * Returns the widget service associated with the session + */ + fun widgetService(): WidgetService + + /** + * Returns the integration manager service associated with the session + */ + fun integrationManagerService(): IntegrationManagerService + /** * Add a listener to the session. * @param listener the listener to add. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index d3780ebe60..1dd8abc858 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -98,7 +98,7 @@ data class Event( * @return true if event is state event. */ fun isStateEvent(): Boolean { - return EventType.isStateEvent(getClearType()) + return stateKey != null } // ============================================================================================================== @@ -162,6 +162,8 @@ data class Event( */ fun isRedactedBySameUser() = senderId == unsignedData?.redactedEvent?.senderId + fun resolvedPrevContent(): Content? = prevContent ?: unsignedData?.prevContent + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 3cdd433516..a33b9e70df 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -38,6 +38,8 @@ object EventType { // State Events + const val STATE_ROOM_WIDGET_LEGACY = "im.vector.modular.widgets" + const val STATE_ROOM_WIDGET = "m.widget" const val STATE_ROOM_NAME = "m.room.name" const val STATE_ROOM_TOPIC = "m.room.topic" const val STATE_ROOM_AVATAR = "m.room.avatar" @@ -84,29 +86,6 @@ object EventType { // Unwedging internal const val DUMMY = "m.dummy" - private val STATE_EVENTS = listOf( - STATE_ROOM_NAME, - STATE_ROOM_TOPIC, - STATE_ROOM_AVATAR, - STATE_ROOM_MEMBER, - STATE_ROOM_THIRD_PARTY_INVITE, - STATE_ROOM_CREATE, - STATE_ROOM_JOIN_RULES, - STATE_ROOM_GUEST_ACCESS, - STATE_ROOM_POWER_LEVELS, - STATE_ROOM_ALIASES, - STATE_ROOM_TOMBSTONE, - STATE_ROOM_CANONICAL_ALIAS, - STATE_ROOM_HISTORY_VISIBILITY, - STATE_ROOM_RELATED_GROUPS, - STATE_ROOM_PINNED_EVENT, - STATE_ROOM_ENCRYPTION - ) - - fun isStateEvent(type: String): Boolean { - return STATE_EVENTS.contains(type) - } - fun isCallEvent(type: String): Boolean { return type == CALL_INVITE || type == CALL_CANDIDATES diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt index 83fb949946..d0cb5332a1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt @@ -16,7 +16,9 @@ package im.vector.matrix.android.api.session.identity -sealed class IdentityServiceError : Throwable() { +import im.vector.matrix.android.api.failure.Failure + +sealed class IdentityServiceError : Failure.FeatureFailure() { object OutdatedIdentityServer : IdentityServiceError() object OutdatedHomeServer : IdentityServiceError() object NoIdentityServerConfigured : IdentityServiceError() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/integrationmanager/IntegrationManagerConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/integrationmanager/IntegrationManagerConfig.kt new file mode 100644 index 0000000000..44a39ae3ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/integrationmanager/IntegrationManagerConfig.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.integrationmanager + +/** + * This class holds configuration of integration manager. + */ +data class IntegrationManagerConfig( + val uiUrl: String, + val restUrl: String, + val kind: Kind +) { + + // Order matters, first is preferred + /** + * The kind of config, it will reflect where the data is coming from. + */ + enum class Kind { + /** + * Defined in UserAccountData + */ + ACCOUNT, + /** + * Defined in Wellknown + */ + HOMESERVER, + /** + * Fallback value, hardcoded by the SDK + */ + DEFAULT + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/integrationmanager/IntegrationManagerService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/integrationmanager/IntegrationManagerService.kt new file mode 100644 index 0000000000..b5b34e2f30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/integrationmanager/IntegrationManagerService.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.integrationmanager + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +/** + * This is the entry point to manage integration. You can grab an instance of this service through an active session. + */ +interface IntegrationManagerService { + + /** + * This listener allow you to observe change related to integrations. + */ + interface Listener { + /** + * Is called whenever integration is enabled or disabled, comes from user account data. + */ + fun onIsEnabledChanged(enabled: Boolean) { + // No-op + } + + /** + * Is called whenever configs from user account data or wellknown are updated. + */ + fun onConfigurationChanged(configs: List) { + // No-op + } + + /** + * Is called whenever widget permissions from user account data are updated. + */ + fun onWidgetPermissionsChanged(widgets: Map) { + // No-op + } + } + + /** + * Adds a listener to observe changes. + */ + fun addListener(listener: Listener) + + /** + * Removes a previously added listener. + */ + fun removeListener(listener: Listener) + + /** + * Return the list of current configurations, sorted by kind. First one is preferred. + * See [IntegrationManagerConfig.Kind] + */ + fun getOrderedConfigs(): List + + /** + * Return the preferred current configuration. + * See [IntegrationManagerConfig.Kind] + */ + fun getPreferredConfig(): IntegrationManagerConfig + + /** + * Returns true if integration is enabled, false otherwise. + */ + fun isIntegrationEnabled(): Boolean + + /** + * Offers to enable or disable the integration. + * @param enable the param to change + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable + + /** + * Offers to allow or disallow a widget. + * @param stateEventId the eventId of the state event defining the widget. + * @param allowed the param to change + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable + + /** + * Returns true if the widget is allowed, false otherwise. + * @param stateEventId the eventId of the state event defining the widget. + */ + fun isWidgetAllowed(stateEventId: String): Boolean + + /** + * Offers to allow or disallow a native widget domain. + * @param widgetType the widget type to check for + * @param domain the domain to check for + */ + fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable + + /** + * Returns true if the widget domain is allowed, false otherwise. + * @param widgetType the widget type to check for + * @param domain the domain to check for + */ + fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean +} 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 2fd7d84f04..4ae61f46e1 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 @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.room.reporting.ReportingService import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService +import im.vector.matrix.android.api.session.room.tags.TagsService import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.typing.TypingService import im.vector.matrix.android.api.session.room.uploads.UploadsService @@ -41,6 +42,7 @@ interface Room : DraftService, ReadService, TypingService, + TagsService, MembershipService, StateService, UploadsService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index 93761dfd26..bc6c17a130 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -73,15 +73,17 @@ interface RoomService { /** * Get a snapshot list of Breadcrumbs + * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance. * @return the immutable list of [RoomSummary] */ - fun getBreadcrumbs(): List + fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List /** * Get a live list of Breadcrumbs + * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance. * @return the [LiveData] of [RoomSummary] */ - fun getBreadcrumbsLive(): LiveData> + fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData> /** * Inform the Matrix SDK that a room is displayed. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index 98bce9476b..f011d317cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -63,6 +63,27 @@ interface MembershipService { reason: String? = null, callback: MatrixCallback): Cancelable + /** + * Ban a user from the room + */ + fun ban(userId: String, + reason: String? = null, + callback: MatrixCallback): Cancelable + + /** + * Unban a user from the room + */ + fun unban(userId: String, + reason: String? = null, + callback: MatrixCallback): Cancelable + + /** + * Kick a user from the room + */ + fun kick(userId: String, + reason: String? = null, + callback: MatrixCallback): Cancelable + /** * Join the room, or accept an invitation. */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt index 7c6a931373..0d5ce70f80 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt @@ -44,6 +44,10 @@ enum class Membership(val value: String) { return this == KNOCK || this == LEAVE || this == BAN } + fun isActive(): Boolean { + return activeMemberships().contains(this) + } + companion object { fun activeMemberships(): List { return listOf(INVITE, JOIN) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevelsContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevelsContent.kt index 2f81965d0a..b52f7b09bf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevelsContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevelsContent.kt @@ -18,21 +18,21 @@ package im.vector.matrix.android.api.session.room.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants +import im.vector.matrix.android.api.session.room.powerlevels.Role /** * Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content. */ @JsonClass(generateAdapter = true) data class PowerLevelsContent( - @Json(name = "ban") val ban: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL, - @Json(name = "kick") val kick: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL, - @Json(name = "invite") val invite: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL, - @Json(name = "redact") val redact: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL, - @Json(name = "events_default") val eventsDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL, + @Json(name = "ban") val ban: Int = Role.Moderator.value, + @Json(name = "kick") val kick: Int = Role.Moderator.value, + @Json(name = "invite") val invite: Int = Role.Moderator.value, + @Json(name = "redact") val redact: Int = Role.Moderator.value, + @Json(name = "events_default") val eventsDefault: Int = Role.Default.value, @Json(name = "events") val events: MutableMap = HashMap(), - @Json(name = "users_default") val usersDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL, + @Json(name = "users_default") val usersDefault: Int = Role.Default.value, @Json(name = "users") val users: MutableMap = HashMap(), - @Json(name = "state_default") val stateDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL, + @Json(name = "state_default") val stateDefault: Int = Role.Moderator.value, @Json(name = "notifications") val notifications: Map = HashMap() ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 1ad6112f2c..3c95fd47fc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -59,6 +59,9 @@ data class RoomSummary constructor( val hasNewMessages: Boolean get() = notificationCount != 0 + val isFavorite: Boolean + get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } + companion object { const val NOT_IN_BREADCRUMBS = -1 } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tag/RoomTag.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tag/RoomTag.kt index 9e85ba3255..6826824278 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tag/RoomTag.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/tag/RoomTag.kt @@ -22,9 +22,8 @@ data class RoomTag( ) { companion object { - val ROOM_TAG_FAVOURITE = "m.favourite" - val ROOM_TAG_LOW_PRIORITY = "m.lowpriority" - val ROOM_TAG_NO_TAG = "m.recent" - val ROOM_TAG_SERVER_NOTICE = "m.server_notice" + const val ROOM_TAG_FAVOURITE = "m.favourite" + const val ROOM_TAG_LOW_PRIORITY = "m.lowpriority" + const val ROOM_TAG_SERVER_NOTICE = "m.server_notice" } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt index b18a7dd97b..49141e3e86 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.api.session.room.powerlevels -import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.model.PowerLevelsContent /** @@ -31,32 +30,84 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { * @param userId the user id * @return the power level */ - fun getUserPowerLevel(userId: String): Int { + fun getUserPowerLevelValue(userId: String): Int { return powerLevelsContent.users.getOrElse(userId) { powerLevelsContent.usersDefault } } + /** + * Returns the user power level of a dedicated user Id + * + * @param userId the user id + * @return the power level + */ + fun getUserRole(userId: String): Role { + val value = getUserPowerLevelValue(userId) + return Role.fromValue(value, powerLevelsContent.eventsDefault) + } + /** * Tell if an user can send an event of a certain type * + * @param userId the id of the user to check for. + * @param isState true if the event is a state event (ie. state key is not null) * @param eventType the event type to check for - * @param userId the user id * @return true if the user can send this type of event */ - fun isAllowedToSend(eventType: String, userId: String): Boolean { - return if (eventType.isNotEmpty() && userId.isNotEmpty()) { - val powerLevel = getUserPowerLevel(userId) + fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean { + return if (userId.isNotEmpty()) { + val powerLevel = getUserPowerLevelValue(userId) val minimumPowerLevel = powerLevelsContent.events[eventType] - ?: if (EventType.isStateEvent(eventType)) { - powerLevelsContent.stateDefault - } else { - powerLevelsContent.eventsDefault - } + ?: if (isState) { + powerLevelsContent.stateDefault + } else { + powerLevelsContent.eventsDefault + } powerLevel >= minimumPowerLevel } else false } + /** + * Check if the user have the necessary power level to invite + * @param userId the id of the user to check for. + * @return true if able to invite + */ + fun isUserAbleToInvite(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.invite + } + + /** + * Check if the user have the necessary power level to ban + * @param userId the id of the user to check for. + * @return true if able to ban + */ + fun isUserAbleToBan(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.ban + } + + /** + * Check if the user have the necessary power level to kick + * @param userId the id of the user to check for. + * @return true if able to kick + */ + fun isUserAbleToKick(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.kick + } + + /** + * Check if the user have the necessary power level to redact + * @param userId the id of the user to check for. + * @return true if able to redact + */ + fun isUserAbleToRedact(userId: String): Boolean { + val powerLevel = getUserPowerLevelValue(userId) + return powerLevel >= powerLevelsContent.redact + } + /** * Get the notification level for a dedicated key. * @@ -68,7 +119,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { // the first implementation was a string value is String -> value.toInt() is Int -> value - else -> PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL + else -> Role.Moderator.value } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/Role.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/Role.kt new file mode 100644 index 0000000000..099adcabdf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/Role.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.matrix.android.api.session.room.powerlevels + +import androidx.annotation.StringRes +import im.vector.matrix.android.R + +sealed class Role(open val value: Int, @StringRes val res: Int) : Comparable { + object Admin : Role(100, R.string.power_level_admin) + object Moderator : Role(50, R.string.power_level_moderator) + object Default : Role(0, R.string.power_level_default) + data class Custom(override val value: Int) : Role(value, R.string.power_level_custom) + + override fun compareTo(other: Role): Int { + return value.compareTo(other.value) + } + + companion object { + + // Order matters, default value should be checked after defined roles + fun fromValue(value: Int, default: Int): Role { + return when (value) { + Admin.value -> Admin + Moderator.value -> Moderator + Default.value, + default -> Default + else -> Custom(value) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index afa3dda496..d0d1d2faf5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.send import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.OptionItem @@ -28,6 +29,14 @@ import im.vector.matrix.android.api.util.Cancelable */ interface SendService { + /** + * Method to send a generic event asynchronously. If you want to send a state event, please use [StateService] instead. + * @param eventType the type of the event + * @param content the optional body as a json dict. + * @return a [Cancelable] + */ + fun sendEvent(eventType: String, content: Content?): Cancelable + /** * Method to send a text message asynchronously. * The text to send can be a Spannable and contains special spans (MatrixItemSpan) that will be translated diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 83c691ebdf..827ce50e13 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -18,7 +18,10 @@ package im.vector.matrix.android.api.session.room.state import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional interface StateService { @@ -26,9 +29,15 @@ interface StateService { /** * Update the topic of the room */ - fun updateTopic(topic: String, callback: MatrixCallback) + fun updateTopic(topic: String, callback: MatrixCallback): Cancelable - fun getStateEvent(eventType: String, stateKey: String): Event? + fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback): Cancelable - fun getStateEventLive(eventType: String, stateKey: String): LiveData> + fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? + + fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData> + + fun getStateEvents(eventTypes: Set, stateKey: QueryStringValue = QueryStringValue.NoCondition): List + + fun getStateEventsLive(eventTypes: Set, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/tags/TagsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/tags/TagsService.kt new file mode 100644 index 0000000000..a4d05224ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/tags/TagsService.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.tags + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +/** + * This interface defines methods to handle tags of a room. It's implemented at the room level. + */ +interface TagsService { + /** + * Add a tag to a room + */ + fun addTag(tag: String, order: Double?, callback: MatrixCallback): Cancelable + + /** + * Remove tag from a room + */ + fun deleteTag(tag: String, callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt index 154074b722..60ccec3074 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt @@ -32,6 +32,10 @@ data class TimelineSettings( * A flag to filter redacted events */ val filterRedacted: Boolean = false, + /** + * A flag to filter useless events, such as membership events without any change + */ + val filterUseless: Boolean = false, /** * A flag to filter by types. It should be used with [allowedTypes] field */ @@ -44,5 +48,4 @@ data class TimelineSettings( * If true, will build read receipts for each event. */ val buildReadReceipts: Boolean = true - ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetManagementFailure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetManagementFailure.kt new file mode 100644 index 0000000000..cd3bdb57ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetManagementFailure.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.widgets + +import im.vector.matrix.android.api.failure.Failure + +sealed class WidgetManagementFailure : Failure.FeatureFailure() { + object NotEnoughPower : WidgetManagementFailure() + object CreationFailed : WidgetManagementFailure() + data class TermsNotSignedException(val baseUrl: String, val token: String) : WidgetManagementFailure() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetPostAPIMediator.kt new file mode 100644 index 0000000000..ac3ed8df09 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetPostAPIMediator.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.widgets + +import android.webkit.WebView +import im.vector.matrix.android.api.util.JsonDict +import java.lang.reflect.Type + +interface WidgetPostAPIMediator { + + /** + * This initialize the webview to handle. + * It will add a JavaScript Interface. + * Please call [clearWebView] method when finished to clean the provided webview + */ + fun setWebView(webView: WebView) + + /** + * Set handler to communicate with the widgetPostAPIMediator. + * Please remove the reference by passing null when finished. + */ + fun setHandler(handler: Handler?) + + /** + * This clear the mediator by removing the JavaScript Interface and cleaning references. + */ + fun clearWebView() + + /** + * Inject the necessary javascript into the configured WebView. + * Should be called after a web page has been loaded. + */ + fun injectAPI() + + /** + * Send a boolean response + * + * @param response the response + * @param eventData the modular data + */ + fun sendBoolResponse(response: Boolean, eventData: JsonDict) + + /** + * Send an integer response + * + * @param response the response + * @param eventData the modular data + */ + fun sendIntegerResponse(response: Int, eventData: JsonDict) + + /** + * Send an object response + * + * @param klass the class of the response + * @param response the response + * @param eventData the modular data + */ + fun sendObjectResponse(type: Type, response: T?, eventData: JsonDict) + + /** + * Send success + * + * @param eventData the modular data + */ + fun sendSuccess(eventData: JsonDict) + + /** + * Send an error + * + * @param message the error message + * @param eventData the modular data + */ + fun sendError(message: String, eventData: JsonDict) + + interface Handler { + /** + * Triggered when a widget is posting + */ + fun handleWidgetRequest(eventData: JsonDict): Boolean + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetService.kt new file mode 100644 index 0000000000..2585d0a968 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetService.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.widgets + +import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.session.widgets.model.Widget + +/** + * This is the entry point to manage widgets. You can grab an instance of this service through an active session. + */ +interface WidgetService { + + /** + * Returns an instance of [WidgetURLFormatter]. + */ + fun getWidgetURLFormatter(): WidgetURLFormatter + + /** + * Returns an instance of [WidgetPostAPIMediator]. + * This is to be used for "admin" widgets so you can interact through JS. + */ + fun getWidgetPostAPIMediator(): WidgetPostAPIMediator + + /** + * Returns the current room widgets defined through state events. + * Some widgets can be deactivated, so be sure to check for isActive if needed. + * + * @param roomId the room where you want to fetch widgets + * @param widgetId if you want to fetch for some particular widget + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getRoomWidgets( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): List + + /** + * Returns the live room widgets so you can listen to them. + * Some widgets can be deactivated, so be sure to check for isActive. + * + * @param roomId the room where you want to fetch widgets + * @param widgetId if you want to fetch for some particular widget + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getRoomWidgetsLive( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): LiveData> + + /** + * Returns the current user widgets. + * Some widgets can be deactivated, so be sure to check for isActive. + * + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getUserWidgets( + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): List + + /** + * Returns the live user widgets so you can listen to them. + * Some widgets can be deactivated, so be sure to check for isActive. + * + * @param widgetTypes if you want to filter some widget type. + * @param excludedTypes if you want to exclude some widget type. + */ + fun getUserWidgetsLive( + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): LiveData> + + /** + * Creates a new widget in a room. It makes sure you have the rights to handle this. + * + * @param roomId: the room where you want to deactivate the widget. + * @param widgetId: the widget to deactivate. + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback): Cancelable + + /** + * Deactivate a widget in a room. It makes sure you have the rights to handle this. + * + * @param roomId: the room where you want to deactivate the widget. + * @param widgetId: the widget to deactivate. + * @param callback the matrix callback to listen for result. + * @return Cancelable + */ + fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback): Cancelable + + /** + * Returns true if you can add/remove widgets. It goes through + * @param roomId the room where you want to administrate widgets. + */ + fun hasPermissionsToHandleWidgets(roomId: String): Boolean +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetURLFormatter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetURLFormatter.kt new file mode 100644 index 0000000000..f2ba40c344 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/WidgetURLFormatter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.widgets + +interface WidgetURLFormatter { + /** + * Takes care of fetching a scalar token if required and build the final url. + * This methods can throw, you should take care of handling failure. + * + * @param baseUrl the baseUrl which will be checked for scalar token + * @param params additional params you want to append to the base url. + * @param forceFetchScalarToken if true, you will force to fetch a new scalar token + * from the server (only if the base url is whitelisted) + * @param bypassWhitelist if true, the base url will be considered as whitelisted + */ + suspend fun format( + baseUrl: String, + params: Map = emptyMap(), + forceFetchScalarToken: Boolean = false, + bypassWhitelist: Boolean + ): String +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/model/Widget.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/model/Widget.kt new file mode 100644 index 0000000000..0b564d6496 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/model/Widget.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.widgets.model + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.sender.SenderInfo + +data class Widget( + val widgetContent: WidgetContent, + val event: Event, + val widgetId: String, + val senderInfo: SenderInfo?, + val isAddedByMe: Boolean, + val computedUrl: String?, + val type: WidgetType +) { + + val isActive = widgetContent.isActive() + + val name = widgetContent.getHumanName() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/model/WidgetContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/model/WidgetContent.kt new file mode 100644 index 0000000000..7e045f25c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/model/WidgetContent.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.widgets.model + +import android.annotation.SuppressLint +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class WidgetContent( + @Json(name = "creatorUserId") val creatorUserId: String? = null, + @Json(name = "id") val id: String? = null, + @Json(name = "type") val type: String? = null, + @Json(name = "url") val url: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "data") val data: JsonDict = emptyMap(), + @Json(name = "waitForIframeLoad") val waitForIframeLoad: Boolean = false +) { + + fun isActive() = type != null && url != null + + @SuppressLint("DefaultLocale") + fun getHumanName(): String { + return (name ?: type ?: "").capitalize() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/model/WidgetType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/model/WidgetType.kt new file mode 100644 index 0000000000..4a265d71b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/widgets/model/WidgetType.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.widgets.model + +sealed class WidgetType(open val preferred: String, open val legacy: String = preferred) { + object Jitsi : WidgetType("m.jitsi", "jitsi") + object TradingView : WidgetType("m.tradingview") + object Spotify : WidgetType("m.spotify") + object Video : WidgetType("m.video") + object GoogleDoc : WidgetType("m.googledoc") + object GoogleCalendar : WidgetType("m.googlecalendar") + object Etherpad : WidgetType("m.etherpad") + object StickerPicker : WidgetType("m.stickerpicker") + object Grafana : WidgetType("m.grafana") + object Custom : WidgetType("m.custom") + object IntegrationManager : WidgetType("m.integration_manager") + data class Fallback(override val preferred: String) : WidgetType(preferred) + + fun matches(type: String?): Boolean { + return type == preferred || type == legacy + } + + fun values(): Set { + return setOf(preferred, legacy) + } + + companion object { + + private val DEFINED_TYPES = listOf( + Jitsi, + TradingView, + Spotify, + Video, + GoogleDoc, + GoogleCalendar, + Etherpad, + StickerPicker, + Grafana, + Custom, + IntegrationManager + ) + + fun fromString(type: String): WidgetType { + val matchingType = DEFINED_TYPES.firstOrNull { + it.matches(type) + } + return matchingType ?: Fallback(type) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt index a0f677d96d..713872e3d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt @@ -47,6 +47,10 @@ data class Optional constructor(private val value: T?) { fun from(value: T?): Optional { return Optional(value) } + + fun empty(): Optional { + return Optional(null) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index 2f03c99421..ec8cc5c074 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -17,16 +17,17 @@ package im.vector.matrix.android.internal.auth import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.RiotConfig +import im.vector.matrix.android.internal.auth.data.TokenLoginParams import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse import im.vector.matrix.android.internal.auth.registration.RegistrationParams import im.vector.matrix.android.internal.auth.registration.SuccessResult import im.vector.matrix.android.internal.auth.registration.ValidationCodeBody +import im.vector.matrix.android.internal.auth.version.Versions import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.Body @@ -54,7 +55,7 @@ internal interface AuthAPI { fun versions(): Call /** - * Register to the homeserver + * Register to the homeserver, or get error 401 with a RegistrationFlowResponse object if registration is incomplete * Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") @@ -91,6 +92,11 @@ internal interface AuthAPI { @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") fun login(@Body loginParams: PasswordLoginParams): Call + // Unfortunately we cannot use interface for @Body parameter, so I duplicate the method for the type TokenLoginParams + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun login(@Body loginParams: TokenLoginParams): Call + /** * Ask the homeserver to reset the password associated with the provided email. */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index b543fa7507..2453bc0d05 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -23,9 +23,6 @@ import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult -import im.vector.matrix.android.api.auth.data.Versions -import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk -import im.vector.matrix.android.api.auth.data.isSupportedBySdk import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.auth.wellknown.WellknownResult @@ -40,6 +37,9 @@ import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard import im.vector.matrix.android.internal.auth.login.DirectLoginTask import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard +import im.vector.matrix.android.internal.auth.version.Versions +import im.vector.matrix.android.internal.auth.version.isLoginAndRegistrationSupportedBySdk +import im.vector.matrix.android.internal.auth.version.isSupportedBySdk import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest @@ -236,7 +236,7 @@ internal class DefaultAuthenticationService @Inject constructor( val loginFlowResponse = executeRequest(null) { apiCall = authAPI.getLoginFlows() } - LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) + LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) } else { // Not supported LoginFlowResult.OutdatedHomeserver diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt index 598506d0a7..ca57a3aa0c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt @@ -20,7 +20,19 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class LoginFlowResponse( +internal data class LoginFlowResponse( + /** + * The homeserver's supported login types + */ @Json(name = "flows") - val flows: List + val flows: List? +) + +@JsonClass(generateAdapter = true) +internal data class LoginFlow( + /** + * The login type. This is supplied as the type when logging in. + */ + @Json(name = "type") + val type: String? ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt index f467b4d3a0..26b1b65218 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/PasswordLoginParams.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.data.LoginFlowTypes /** * Ref: diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/TokenLoginParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/TokenLoginParams.kt new file mode 100644 index 0000000000..e95a998d38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/TokenLoginParams.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.data.LoginFlowTypes + +@JsonClass(generateAdapter = true) +internal data class TokenLoginParams( + @Json(name = "type") override val type: String = LoginFlowTypes.TOKEN, + @Json(name = "token") val token: String +) : LoginParams diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt index 132073b340..2ce9372903 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.auth.PendingSessionStore import im.vector.matrix.android.internal.auth.SessionCreator import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.ThreePidMedium +import im.vector.matrix.android.internal.auth.data.TokenLoginParams import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse @@ -65,6 +66,22 @@ internal class DefaultLoginWizard( } } + /** + * Ref: https://matrix.org/docs/spec/client_server/latest#handling-the-authentication-endpoint + */ + override fun loginWithToken(loginToken: String, callback: MatrixCallback): Cancelable { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { + val loginParams = TokenLoginParams( + token = loginToken + ) + val credentials = executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + + sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig) + } + } + private suspend fun loginInternal(login: String, password: String, deviceName: String) = withContext(coroutineDispatchers.computation) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt index ad85579550..9e20be240a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/AuthParams.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.auth.registration import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.api.auth.data.LoginFlowTypes /** * Open class, parent to all possible authentication parameters diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index 5a39de72ca..750d806b6f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.registration import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.auth.registration.RegisterThreePid import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.auth.registration.RegistrationWizard @@ -28,7 +29,6 @@ import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.auth.AuthAPI import im.vector.matrix.android.internal.auth.PendingSessionStore import im.vector.matrix.android.internal.auth.SessionCreator -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.task.launchToCallback diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt index dd5a553193..5bb87f9557 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/RegistrationFlowResponse.kt @@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.auth.registration import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.Stage import im.vector.matrix.android.api.auth.registration.TermPolicies import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes @JsonClass(generateAdapter = true) data class RegistrationFlowResponse( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/version/HomeServerVersion.kt new file mode 100644 index 0000000000..7e0a95dfef --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/version/HomeServerVersion.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.version + +/** + * Values will take the form "rX.Y.Z". + * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions + */ +internal data class HomeServerVersion( + val major: Int, + val minor: Int, + val patch: Int +) : Comparable { + override fun compareTo(other: HomeServerVersion): Int { + return when { + major > other.major -> 1 + major < other.major -> -1 + minor > other.minor -> 1 + minor < other.minor -> -1 + patch > other.patch -> 1 + patch < other.patch -> -1 + else -> 0 + } + } + + companion object { + internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""") + + internal fun parse(value: String): HomeServerVersion? { + val result = pattern.matchEntire(value) ?: return null + return HomeServerVersion( + major = result.groupValues[1].toInt(), + minor = result.groupValues[2].toInt(), + patch = result.groupValues[3].toInt() + ) + } + + val r0_0_0 = HomeServerVersion(major = 0, minor = 0, patch = 0) + val r0_1_0 = HomeServerVersion(major = 0, minor = 1, patch = 0) + val r0_2_0 = HomeServerVersion(major = 0, minor = 2, patch = 0) + val r0_3_0 = HomeServerVersion(major = 0, minor = 3, patch = 0) + val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0) + val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0) + val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Versions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/version/Versions.kt similarity index 80% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Versions.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/version/Versions.kt index c4186c6ec5..aee5f7e3f3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Versions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/version/Versions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.auth.data +package im.vector.matrix.android.internal.auth.version import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -38,7 +38,7 @@ import com.squareup.moshi.JsonClass * */ @JsonClass(generateAdapter = true) -data class Versions( +internal data class Versions( @Json(name = "versions") val supportedVersions: List? = null, @@ -46,15 +46,6 @@ data class Versions( val unstableFeatures: Map? = null ) -// MatrixClientServerAPIVersion -private const val r0_0_1 = "r0.0.1" -private const val r0_1_0 = "r0.1.0" -private const val r0_2_0 = "r0.2.0" -private const val r0_3_0 = "r0.3.0" -private const val r0_4_0 = "r0.4.0" -private const val r0_5_0 = "r0.5.0" -private const val r0_6_0 = "r0.6.0" - // MatrixVersionsFeature private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members" private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server" @@ -64,14 +55,14 @@ private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind" /** * Return true if the SDK supports this homeserver version */ -fun Versions.isSupportedBySdk(): Boolean { +internal fun Versions.isSupportedBySdk(): Boolean { return supportLazyLoadMembers() } /** * Return true if the SDK supports this homeserver version for login and registration */ -fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean { +internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean { return !doesServerRequireIdentityServerParam() && doesServerAcceptIdentityAccessToken() && doesServerSeparatesAddAndBind() @@ -83,7 +74,7 @@ fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean { * @return true if the server support the lazy loading of room members */ private fun Versions.supportLazyLoadMembers(): Boolean { - return supportedVersions?.contains(r0_5_0) == true + return getMaxVersion() >= HomeServerVersion.r0_5_0 || unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true } @@ -92,7 +83,7 @@ private fun Versions.supportLazyLoadMembers(): Boolean { * adding a 3pid or resetting password. */ private fun Versions.doesServerRequireIdentityServerParam(): Boolean { - if (supportedVersions?.contains(r0_6_0) == true) return false + if (getMaxVersion() >= HomeServerVersion.r0_6_0) return false return unstableFeatures?.get(FEATURE_REQUIRE_IDENTITY_SERVER) ?: true } @@ -101,11 +92,18 @@ private fun Versions.doesServerRequireIdentityServerParam(): Boolean { * Some homeservers may trigger errors if they are not prepared for the new parameter. */ private fun Versions.doesServerAcceptIdentityAccessToken(): Boolean { - return supportedVersions?.contains(r0_6_0) == true + return getMaxVersion() >= HomeServerVersion.r0_6_0 || unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false } private fun Versions.doesServerSeparatesAddAndBind(): Boolean { - return supportedVersions?.contains(r0_6_0) == true + return getMaxVersion() >= HomeServerVersion.r0_6_0 || unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false } + +private fun Versions.getMaxVersion(): HomeServerVersion { + return supportedVersions + ?.mapNotNull { HomeServerVersion.parse(it) } + ?.max() + ?: HomeServerVersion.r0_0_0 +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt index da2dd781dd..758967d05e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt @@ -36,7 +36,7 @@ internal data class KeysQueryBody( * A map from user ID, to a list of device IDs, or to an empty list to indicate all devices for the corresponding user. */ @Json(name = "device_keys") - val deviceKeys: Map, + val deviceKeys: Map>, /** * If the client is fetching keys as a result of a device update received in a sync request, this should be the 'since' token diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt index 5e672d4f59..0e0fa9002d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.api.auth.data.LoginFlowTypes /** * This class provides the authentication data by using user and password diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt index 940fa9c7fb..ef9a9ead8e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.crypto.tasks -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt index 94fe3c1e8d..5e4b2184b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -27,7 +27,7 @@ import javax.inject.Inject internal interface DownloadKeysForUsersTask : Task { data class Params( // the list of users to get keys for. - val userIds: List?, + val userIds: List, // the up-to token val token: String? ) @@ -39,7 +39,7 @@ internal class DefaultDownloadKeysForUsers @Inject constructor( ) : DownloadKeysForUsersTask { override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse { - val downloadQuery = params.userIds?.associateWith { emptyMap() }.orEmpty() + val downloadQuery = params.userIds.associateWith { emptyList() } val body = KeysQueryBody( deviceKeys = downloadQuery, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/AccountDataMapper.kt similarity index 88% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/AccountDataMapper.kt index 3a736681e2..528712b731 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/AccountDataMapper.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.identity.todelete +package im.vector.matrix.android.internal.database.mapper import com.squareup.moshi.Moshi import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE @@ -22,7 +22,6 @@ import im.vector.matrix.android.internal.database.model.UserAccountDataEntity import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import javax.inject.Inject -// There will be a duplicated class when Integration manager will be merged, so delete this one internal class AccountDataMapper @Inject constructor(moshi: Moshi) { private val adapter = moshi.adapter>(JSON_DICT_PARAMETERIZED_TYPE) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index e6a082c720..e28de760fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -36,8 +36,8 @@ internal object EventMapper { eventEntity.eventId = event.eventId ?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}" eventEntity.roomId = event.roomId ?: roomId eventEntity.content = ContentMapper.map(event.content) - val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent - eventEntity.prevContent = ContentMapper.map(resolvedPrevContent) + eventEntity.prevContent = ContentMapper.map(event.resolvedPrevContent()) + eventEntity.isUseless = IsUselessResolver.isUseless(event) eventEntity.stateKey = event.stateKey eventEntity.type = event.type eventEntity.sender = event.senderId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/IsUselessResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/IsUselessResolver.kt new file mode 100644 index 0000000000..56ce488f1b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/IsUselessResolver.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.mapper + +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.toContent + +internal object IsUselessResolver { + + /** + * @return true if the event is useless + */ + fun isUseless(event: Event): Boolean { + return when (event.type) { + EventType.STATE_ROOM_MEMBER -> { + // Call toContent(), to filter out null value + event.content != null + && event.content.toContent() == event.resolvedPrevContent()?.toContent() + } + else -> false + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 72015afc43..7e69e84840 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -28,6 +28,7 @@ internal open class EventEntity(@Index var eventId: String = "", @Index var type: String = "", var content: String? = null, var prevContent: String? = null, + var isUseless: Boolean = false, @Index var stateKey: String? = null, var originServerTs: Long? = null, @Index var sender: String? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Strings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ScalarTokenEntity.kt similarity index 63% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Strings.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ScalarTokenEntity.kt index 09913b9f04..eab85357e3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/Strings.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ScalarTokenEntity.kt @@ -14,11 +14,15 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.extensions +package im.vector.matrix.android.internal.database.model -/** - * Ex: "abcdef".subStringBetween("a", "f") -> "bcde" - * Ex: "abcdefff".subStringBetween("a", "f") -> "bcdeff" - * Ex: "aaabcdef".subStringBetween("a", "f") -> "aabcde" - */ -internal fun String.subStringBetween(prefix: String, suffix: String) = substringAfter(prefix).substringBeforeLast(suffix) +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class ScalarTokenEntity( + @PrimaryKey var serverUrl: String = "", + var token: String = "" +) : RealmObject() { + + 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 accac9ca97..9eceb56141 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 @@ -55,6 +55,8 @@ import io.realm.annotations.RealmModule HomeServerCapabilitiesEntity::class, RoomMemberSummaryEntity::class, CurrentStateEventEntity::class, - UserAccountDataEntity::class + UserAccountDataEntity::class, + ScalarTokenEntity::class, + WellknownIntegrationManagerConfigEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/WellknownIntegrationManagerConfigEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/WellknownIntegrationManagerConfigEntity.kt new file mode 100644 index 0000000000..56d66782aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/WellknownIntegrationManagerConfigEntity.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class WellknownIntegrationManagerConfigEntity( + @PrimaryKey var id: Long = 0, + var apiUrl: String = "", + var uiUrl: String = "" +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt index 814e38276a..45f33ae8df 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/CurrentStateEventEntityQueries.kt @@ -23,7 +23,7 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.createObject -internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: String, type: String): RealmQuery { +internal fun CurrentStateEventEntity.Companion.whereType(realm: Realm, roomId: String, type: String): RealmQuery { return realm.where(CurrentStateEventEntity::class.java) .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) .equalTo(CurrentStateEventEntityFields.TYPE, type) @@ -31,7 +31,7 @@ internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: Strin internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String) : RealmQuery { - return where(realm = realm, roomId = roomId, type = type) + return whereType(realm = realm, roomId = roomId, type = type) .equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ScalarTokenQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ScalarTokenQuery.kt new file mode 100644 index 0000000000..02210394f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ScalarTokenQuery.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.query + +import im.vector.matrix.android.internal.database.model.ScalarTokenEntity +import im.vector.matrix.android.internal.database.model.ScalarTokenEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ScalarTokenEntity.Companion.where(realm: Realm, serverUrl: String): RealmQuery { + return realm + .where() + .equalTo(ScalarTokenEntityFields.SERVER_URL, serverUrl) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/LiveData.kt similarity index 86% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/LiveData.kt index f84756fa86..55c9e238e6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/extensions/LiveData.kt @@ -14,13 +14,12 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.identity.todelete +package im.vector.matrix.android.internal.extensions import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -// There will be a duplicated class when Integration manager will be merged, so delete this one inline fun LiveData.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) { this.observe(owner, Observer { observer(it) }) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt index 56e6ee0953..9fb8b463a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -31,6 +31,5 @@ internal object NetworkConstants { const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2" const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/" - // TODO Ganfra, use correct value - const val URI_INTEGRATION_MANAGER_PATH = "TODO/" + const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/" } 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 44ccd7c941..ad26171793 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 @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.network import com.squareup.moshi.Moshi import dagger.Lazy +import im.vector.matrix.android.internal.util.ensureTrailingSlash import okhttp3.Call import okhttp3.OkHttpClient import okhttp3.Request @@ -29,7 +30,7 @@ class RetrofitFactory @Inject constructor(private val moshi: Moshi) { fun create(okHttpClient: Lazy, baseUrl: String): Retrofit { return Retrofit.Builder() - .baseUrl(baseUrl) + .baseUrl(baseUrl.ensureTrailingSlash()) .callFactory(object : Call.Factory { override fun newCall(request: Request): Call { return okHttpClient.get().newCall(request) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 2a96784451..d62eb7b505 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService @@ -45,6 +46,7 @@ import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.user.UserService +import im.vector.matrix.android.api.session.widgets.WidgetService import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater @@ -56,6 +58,7 @@ import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecr import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncWorker +import im.vector.matrix.android.internal.session.widgets.WidgetDependenciesHolder import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.Dispatchers @@ -90,6 +93,7 @@ internal class DefaultSession @Inject constructor( private val fileService: Lazy, private val secureStorageService: Lazy, private val profileService: Lazy, + private val widgetService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, private val syncTokenStore: SyncTokenStore, @@ -101,11 +105,13 @@ internal class DefaultSession @Inject constructor( private val _sharedSecretStorageService: Lazy, private val accountService: Lazy, private val timelineEventDecryptor: TimelineEventDecryptor, - private val shieldTrustUpdater: ShieldTrustUpdater, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val defaultIdentityService: DefaultIdentityService, - private val taskExecutor: TaskExecutor -) : Session, + private val integrationManagerService: IntegrationManagerService, + private val taskExecutor: TaskExecutor, + private val widgetDependenciesHolder: WidgetDependenciesHolder, + private val shieldTrustUpdater: ShieldTrustUpdater) + : Session, RoomService by roomService.get(), RoomDirectoryService by roomDirectoryService.get(), GroupService by groupService.get(), @@ -142,6 +148,7 @@ internal class DefaultSession @Inject constructor( timelineEventDecryptor.start() shieldTrustUpdater.start() defaultIdentityService.start() + widgetDependenciesHolder.start() } override fun requireBackgroundSync() { @@ -187,6 +194,7 @@ internal class DefaultSession @Inject constructor( taskExecutor.executorScope.launch(coroutineDispatchers.main) { // This has to be done on main thread defaultIdentityService.stop() + widgetDependenciesHolder.stop() } } @@ -233,6 +241,10 @@ internal class DefaultSession @Inject constructor( override fun identityService() = defaultIdentityService + override fun widgetService(): WidgetService = widgetService.get() + + override fun integrationManagerService() = integrationManagerService + override fun addListener(listener: Session.Listener) { sessionListeners.addListener(listener) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index ca8ab42ab8..5b64f2a60a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -37,6 +37,7 @@ import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.group.GroupModule import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule import im.vector.matrix.android.internal.session.identity.IdentityModule +import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManagerModule import im.vector.matrix.android.internal.session.openid.OpenIdModule import im.vector.matrix.android.internal.session.profile.ProfileModule import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker @@ -55,6 +56,7 @@ import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.terms.TermsModule import im.vector.matrix.android.internal.session.user.UserModule import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule +import im.vector.matrix.android.internal.session.widgets.WidgetModule import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @@ -74,6 +76,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers CryptoModule::class, PushersModule::class, OpenIdModule::class, + WidgetModule::class, + IntegrationManagerModule::class, IdentityModule::class, TermsModule::class, AccountDataModule::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt index df12ad6131..f039628fd4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt @@ -19,16 +19,16 @@ package im.vector.matrix.android.internal.session.content import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.util.ensureTrailingSlash import javax.inject.Inject private const val MATRIX_CONTENT_URI_SCHEME = "mxc://" internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver { - private val baseUrl = homeServerConnectionConfig.homeServerUri.toString() - private val sep = if (baseUrl.endsWith("/")) "" else "/" + private val baseUrl = homeServerConnectionConfig.homeServerUri.toString().ensureTrailingSlash() - override val uploadUrl = baseUrl + sep + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload" + override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload" override fun resolveFullSize(contentUrl: String?): String? { return contentUrl @@ -66,7 +66,7 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset) } - return baseUrl + sep + prefix + serverAndMediaId + params + fragment + return baseUrl + prefix + serverAndMediaId + params + fragment } private fun String.isValidMatrixContentUrl(): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt index 880a8fbc31..f96863bea6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.session.homeserver -import im.vector.matrix.android.api.auth.data.Versions +import im.vector.matrix.android.internal.auth.version.Versions import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.GET 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 be5b0d3949..f1ee8b4583 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 @@ -17,18 +17,20 @@ package im.vector.matrix.android.internal.session.homeserver import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.auth.data.Versions -import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities -import im.vector.matrix.android.internal.wellknown.GetWellknownTask +import im.vector.matrix.android.internal.auth.version.Versions +import im.vector.matrix.android.internal.auth.version.isLoginAndRegistrationSupportedBySdk import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManagerConfigExtractor import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction +import im.vector.matrix.android.internal.wellknown.GetWellknownTask import org.greenrobot.eventbus.EventBus +import timber.log.Timber import java.util.Date import javax.inject.Inject @@ -39,6 +41,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( private val monarchy: Monarchy, private val eventBus: EventBus, private val getWellknownTask: GetWellknownTask, + private val configExtractor: IntegrationManagerConfigExtractor, @UserId private val userId: String ) : GetHomeServerCapabilitiesTask { @@ -102,8 +105,14 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl - } + // We are also checking for integration manager configurations + val config = configExtractor.extract(getWellknownResult.wellKnown) + if (config != null) { + Timber.v("Extracted integration config : $config") + realm.insertOrUpdate(config) + } + } homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt index 1a271e659e..00c4c48a6d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt @@ -37,16 +37,16 @@ import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.di.AuthenticatedIdentity import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.extensions.observeNotNull import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.identity.data.IdentityStore -import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource -import im.vector.matrix.android.internal.session.identity.todelete.observeNotNull import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask import im.vector.matrix.android.internal.session.profile.BindThreePidsTask import im.vector.matrix.android.internal.session.profile.UnbindThreePidsTask import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource 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.launchToCallback diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/AllowedWidgetsContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/AllowedWidgetsContent.kt new file mode 100644 index 0000000000..99959f9514 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/AllowedWidgetsContent.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.integrationmanager + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AllowedWidgetsContent( + /** + * Map of stateEventId to Allowed + */ + @Json(name = "widgets") val widgets: Map = emptyMap(), + + /** + * Map of native widgetType to a map of domain to Allowed + * { + * "jitsi" : { + * "jitsi.domain.org" : true, + * "jitsi.other.org" : false + * } + * } + */ + @Json(name = "native_widgets") val native: Map> = emptyMap() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/DefaultIntegrationManagerService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/DefaultIntegrationManagerService.kt new file mode 100644 index 0000000000..4de4a24738 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/DefaultIntegrationManagerService.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.integrationmanager + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService +import im.vector.matrix.android.api.util.Cancelable +import javax.inject.Inject + +internal class DefaultIntegrationManagerService @Inject constructor(private val integrationManager: IntegrationManager) : IntegrationManagerService { + + override fun addListener(listener: IntegrationManagerService.Listener) { + integrationManager.addListener(listener) + } + + override fun removeListener(listener: IntegrationManagerService.Listener) { + integrationManager.removeListener(listener) + } + + override fun getOrderedConfigs(): List { + return integrationManager.getOrderedConfigs() + } + + override fun getPreferredConfig(): IntegrationManagerConfig { + return integrationManager.getPreferredConfig() + } + + override fun isIntegrationEnabled(): Boolean { + return integrationManager.isIntegrationEnabled() + } + + override fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable { + return integrationManager.setIntegrationEnabled(enable, callback) + } + + override fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + return integrationManager.setWidgetAllowed(stateEventId, allowed, callback) + } + + override fun isWidgetAllowed(stateEventId: String): Boolean { + return integrationManager.isWidgetAllowed(stateEventId) + } + + override fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + return integrationManager.setNativeWidgetDomainAllowed(widgetType, domain, allowed, callback) + } + + override fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean { + return integrationManager.isNativeWidgetDomainAllowed(widgetType, domain) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt new file mode 100644 index 0000000000..c3e19b6540 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.integrationmanager + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.MatrixConfiguration +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService +import im.vector.matrix.android.api.session.widgets.model.WidgetContent +import im.vector.matrix.android.api.session.widgets.model.WidgetType +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable +import im.vector.matrix.android.internal.database.model.WellknownIntegrationManagerConfigEntity +import im.vector.matrix.android.internal.extensions.observeNotNull +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask +import im.vector.matrix.android.internal.session.widgets.helper.WidgetFactory +import im.vector.matrix.android.internal.session.widgets.helper.extractWidgetSequence +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber +import javax.inject.Inject + +/** + * The integration manager allows to + * - Get the Integration Manager that a user has explicitly set for its account (via account data) + * - Get the recommended/preferred Integration Manager list as defined by the HomeServer (via wellknown) + * - Check if the user has disabled the integration manager feature + * - Allow / Disallow Integration manager (propagated to other riot clients) + * + * The integration manager listen to account data, and can notify observer for changes. + * + * The wellknown is refreshed at each application fresh start + * + */ +@SessionScope +internal class IntegrationManager @Inject constructor(matrixConfiguration: MatrixConfiguration, + private val taskExecutor: TaskExecutor, + private val monarchy: Monarchy, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val accountDataDataSource: AccountDataDataSource, + private val widgetFactory: WidgetFactory) { + + private val currentConfigs = ArrayList() + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val listeners = HashSet() + fun addListener(listener: IntegrationManagerService.Listener) = synchronized(listeners) { listeners.add(listener) } + fun removeListener(listener: IntegrationManagerService.Listener) = synchronized(listeners) { listeners.remove(listener) } + + init { + val defaultConfig = IntegrationManagerConfig( + uiUrl = matrixConfiguration.integrationUIUrl, + restUrl = matrixConfiguration.integrationRestUrl, + kind = IntegrationManagerConfig.Kind.DEFAULT + ) + currentConfigs.add(defaultConfig) + } + + fun start() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + observeWellknownConfig() + accountDataDataSource + .getLiveAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) + .observeNotNull(lifecycleOwner) { + val allowedWidgetsContent = it.getOrNull()?.content?.toModel() + if (allowedWidgetsContent != null) { + notifyWidgetPermissionsChanged(allowedWidgetsContent) + } + } + accountDataDataSource + .getLiveAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING) + .observeNotNull(lifecycleOwner) { + val integrationProvisioningContent = it.getOrNull()?.content?.toModel() + if (integrationProvisioningContent != null) { + notifyIsEnabledChanged(integrationProvisioningContent) + } + } + accountDataDataSource + .getLiveAccountDataEvent(UserAccountData.TYPE_WIDGETS) + .observeNotNull(lifecycleOwner) { + val integrationManagerContent = it.getOrNull()?.asIntegrationManagerWidgetContent() + val config = integrationManagerContent?.extractIntegrationManagerConfig() + updateCurrentConfigs(IntegrationManagerConfig.Kind.ACCOUNT, config) + } + } + + fun stop() { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + fun hasConfig() = currentConfigs.isNotEmpty() + + fun getOrderedConfigs(): List { + return currentConfigs.sortedBy { + it.kind + } + } + + fun getPreferredConfig(): IntegrationManagerConfig { + // This can't be null as we should have at least the default one registered + return getOrderedConfigs().first() + } + + /** + * Returns false if the user as disabled integration manager feature + */ + fun isIntegrationEnabled(): Boolean { + val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING) + val integrationProvisioningContent = integrationProvisioningData?.content?.toModel() + return integrationProvisioningContent?.enabled ?: false + } + + fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable { + val isIntegrationEnabled = isIntegrationEnabled() + if (enable == isIntegrationEnabled) { + callback.onSuccess(Unit) + return NoOpCancellable + } + val integrationProvisioningContent = IntegrationProvisioningContent(enabled = enable) + val params = UpdateUserAccountDataTask.IntegrationProvisioning(integrationProvisioningContent = integrationProvisioningContent) + return updateUserAccountDataTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel() + val newContent = if (currentContent == null) { + val allowedWidget = mapOf(stateEventId to allowed) + AllowedWidgetsContent(widgets = allowedWidget, native = emptyMap()) + } else { + val allowedWidgets = currentContent.widgets.toMutableMap().apply { + put(stateEventId, allowed) + } + currentContent.copy(widgets = allowedWidgets) + } + val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent) + return updateUserAccountDataTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + fun isWidgetAllowed(stateEventId: String): Boolean { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel() + return currentContent?.widgets?.get(stateEventId) ?: false + } + + fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel() + val newContent = if (currentContent == null) { + val nativeAllowedWidgets = mapOf(widgetType to mapOf(domain to allowed)) + AllowedWidgetsContent(widgets = emptyMap(), native = nativeAllowedWidgets) + } else { + val nativeAllowedWidgets = currentContent.native.toMutableMap().apply { + (get(widgetType))?.let { + set(widgetType, it.toMutableMap().apply { set(domain, allowed) }) + } ?: run { + set(widgetType, mapOf(domain to allowed)) + } + } + currentContent.copy(native = nativeAllowedWidgets) + } + val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent) + return updateUserAccountDataTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + fun isNativeWidgetDomainAllowed(widgetType: String, domain: String?): Boolean { + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) + val currentContent = currentAllowedWidgets?.content?.toModel() + return currentContent?.native?.get(widgetType)?.get(domain) ?: false + } + + private fun notifyConfigurationChanged() { + synchronized(listeners) { + listeners.forEach { + try { + it.onConfigurationChanged(currentConfigs) + } catch (t: Throwable) { + Timber.e(t, "Failed to notify listener") + } + } + } + } + + private fun notifyWidgetPermissionsChanged(allowedWidgets: AllowedWidgetsContent) { + Timber.v("On widget permissions changed: $allowedWidgets") + synchronized(listeners) { + listeners.forEach { + try { + it.onWidgetPermissionsChanged(allowedWidgets.widgets) + } catch (t: Throwable) { + Timber.e(t, "Failed to notify listener") + } + } + } + } + + private fun notifyIsEnabledChanged(provisioningContent: IntegrationProvisioningContent) { + Timber.v("On provisioningContent changed : $provisioningContent") + synchronized(listeners) { + listeners.forEach { + try { + it.onIsEnabledChanged(provisioningContent.enabled) + } catch (t: Throwable) { + Timber.e(t, "Failed to notify listener") + } + } + } + } + + private fun WidgetContent.extractIntegrationManagerConfig(): IntegrationManagerConfig? { + if (url.isNullOrBlank()) { + return null + } + val integrationManagerData = data.toModel() + return IntegrationManagerConfig( + uiUrl = url, + restUrl = integrationManagerData?.apiUrl ?: url, + kind = IntegrationManagerConfig.Kind.ACCOUNT + ) + } + + private fun UserAccountDataEvent.asIntegrationManagerWidgetContent(): WidgetContent? { + return extractWidgetSequence(widgetFactory) + .filter { + WidgetType.IntegrationManager == it.type + } + .firstOrNull()?.widgetContent + } + + private fun observeWellknownConfig() { + val liveData = monarchy.findAllMappedWithChanges( + { it.where(WellknownIntegrationManagerConfigEntity::class.java) }, + { IntegrationManagerConfig(it.uiUrl, it.apiUrl, IntegrationManagerConfig.Kind.HOMESERVER) } + ) + liveData.observeNotNull(lifecycleOwner) { + val config = it.firstOrNull() + updateCurrentConfigs(IntegrationManagerConfig.Kind.HOMESERVER, config) + } + } + + private fun updateCurrentConfigs(kind: IntegrationManagerConfig.Kind, config: IntegrationManagerConfig?) { + val hasBeenRemoved = currentConfigs.removeAll { currentConfig -> + currentConfig.kind == kind + } + if (config != null) { + currentConfigs.add(config) + } + if (hasBeenRemoved || config != null) { + notifyConfigurationChanged() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt new file mode 100644 index 0000000000..cd2621ab13 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManagerConfigExtractor.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.integrationmanager + +import im.vector.matrix.android.api.auth.data.WellKnown +import im.vector.matrix.android.internal.database.model.WellknownIntegrationManagerConfigEntity +import javax.inject.Inject + +internal class IntegrationManagerConfigExtractor @Inject constructor() { + + fun extract(wellKnown: WellKnown): WellknownIntegrationManagerConfigEntity? { + wellKnown.integrations?.get("managers")?.let { + (it as? List<*>)?.let { configs -> + configs.forEach { config -> + (config as? Map<*, *>)?.let { map -> + val apiUrl = map["api_url"] as? String + val uiUrl = map["ui_url"] as? String ?: apiUrl + if (apiUrl != null + && apiUrl.startsWith("https://") + && uiUrl!!.startsWith("https://")) { + return WellknownIntegrationManagerConfigEntity( + apiUrl = apiUrl, + uiUrl = uiUrl + ) + } + } + } + } + } + return null + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManagerModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManagerModule.kt new file mode 100644 index 0000000000..1c29f9c38c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManagerModule.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.integrationmanager + +import dagger.Binds +import dagger.Module +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService + +@Module +internal abstract class IntegrationManagerModule { + + @Binds + abstract fun bindIntegrationManagerService(service: DefaultIntegrationManagerService): IntegrationManagerService +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManagerWidgetData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManagerWidgetData.kt new file mode 100644 index 0000000000..1b77b21bc2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManagerWidgetData.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.integrationmanager + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IntegrationManagerWidgetData( + @Json(name = "api_url") val apiUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationProvisioningContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationProvisioningContent.kt new file mode 100644 index 0000000000..51485df762 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationProvisioningContent.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.integrationmanager + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IntegrationProvisioningContent( + @Json(name = "enabled") val enabled: Boolean +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultConditionResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultConditionResolver.kt index f130cf5bc7..7490ca54ad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultConditionResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultConditionResolver.kt @@ -48,7 +48,7 @@ internal class DefaultConditionResolver @Inject constructor( val roomId = event.roomId ?: return false val room = roomGetter.getRoom(roomId) ?: return false - val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "") + val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) ?.content ?.toModel() ?: PowerLevelsContent() 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 40d0500a48..04d495211e 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 @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.reporting.ReportingService import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService +import im.vector.matrix.android.api.session.room.tags.TagsService import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.typing.TypingService import im.vector.matrix.android.api.session.room.uploads.UploadsService @@ -59,6 +60,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val reportingService: ReportingService, private val readService: ReadService, private val typingService: TypingService, + private val tagsService: TagsService, private val cryptoService: CryptoService, private val relationService: RelationService, private val roomMembersService: MembershipService, @@ -74,6 +76,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, ReportingService by reportingService, ReadService by readService, TypingService by typingService, + TagsService by tagsService, RelationService by relationService, MembershipService by roomMembersService, RoomPushRuleService by roomPushRuleService { @@ -116,9 +119,11 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported")) } else -> { - val params = SendStateTask.Params(roomId, - EventType.STATE_ROOM_ENCRYPTION, - mapOf( + val params = SendStateTask.Params( + roomId = roomId, + stateKey = null, + eventType = EventType.STATE_ROOM_ENCRYPTION, + body = mapOf( "algorithm" to algorithm )) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 84fc357160..8868ab4fe1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -111,24 +111,22 @@ internal class DefaultRoomService @Inject constructor( return query } - override fun getBreadcrumbs(): List { + override fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List { return monarchy.fetchAllMappedSync( - { breadcrumbsQuery(it) }, + { breadcrumbsQuery(it, queryParams) }, { roomSummaryMapper.map(it) } ) } - override fun getBreadcrumbsLive(): LiveData> { + override fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData> { return monarchy.findAllMappedWithChanges( - { breadcrumbsQuery(it) }, + { breadcrumbsQuery(it, queryParams) }, { roomSummaryMapper.map(it) } ) } - private fun breadcrumbsQuery(realm: Realm): RealmQuery { - return RoomSummaryEntity.where(realm) - .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + private fun breadcrumbsQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { + return roomSummariesQuery(realm, queryParams) .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS) .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index f5ddf6ae4b..e01e58856f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -24,18 +24,22 @@ import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol +import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse +import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody import im.vector.matrix.android.internal.session.room.relation.RelationsResponse import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody import im.vector.matrix.android.internal.session.room.send.SendResponse +import im.vector.matrix.android.internal.session.room.tags.TagBody import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse import im.vector.matrix.android.internal.session.room.typing.TypingBody import retrofit2.Call import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Headers import retrofit2.http.POST @@ -175,7 +179,7 @@ internal interface RoomAPI { @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}") fun sendStateEvent(@Path("roomId") roomId: String, @Path("state_event_type") stateEventType: String, - @Body params: Map): Call + @Body params: JsonDict): Call /** * Send a generic state events @@ -189,7 +193,7 @@ internal interface RoomAPI { fun sendStateEvent(@Path("roomId") roomId: String, @Path("state_event_type") stateEventType: String, @Path("state_key") stateKey: String, - @Body params: Map): Call + @Body params: JsonDict): Call /** * Send a relation event to a room. @@ -242,6 +246,33 @@ internal interface RoomAPI { fun leave(@Path("roomId") roomId: String, @Body params: Map): Call + /** + * Ban a user from the given room. + * + * @param roomId the room id + * @param userIdAndReason the banned user object (userId and reason for ban) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/ban") + fun ban(@Path("roomId") roomId: String, @Body userIdAndReason: UserIdAndReason): Call + + /** + * unban a user from the given room. + * + * @param roomId the room id + * @param userIdAndReason the unbanned user object (userId and reason for unban) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/unban") + fun unban(@Path("roomId") roomId: String, @Body userIdAndReason: UserIdAndReason): Call + + /** + * Kick a user from the given room. + * + * @param roomId the room id + * @param userIdAndReason the kicked user object (userId and reason for kicking) + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick") + fun kick(@Path("roomId") roomId: String, @Body userIdAndReason: UserIdAndReason): Call + /** * Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room. * This cannot be undone. @@ -287,4 +318,25 @@ internal interface RoomAPI { fun sendTypingState(@Path("roomId") roomId: String, @Path("userId") userId: String, @Body body: TypingBody): Call + + /** + * Room tagging + */ + + /** + * Add a tag to a room. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}") + fun putTag(@Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("tag") tag: String, + @Body body: TagBody): Call + + /** + * Delete a tag from a room. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}") + fun deleteTag(@Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("tag") tag: String): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 974c30dba9..0560aa80c2 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 @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.session.room.reporting.DefaultReporting import im.vector.matrix.android.internal.session.room.send.DefaultSendService import im.vector.matrix.android.internal.session.room.state.DefaultStateService import im.vector.matrix.android.internal.session.room.state.SendStateTask +import im.vector.matrix.android.internal.session.room.tags.DefaultTagsService import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService import im.vector.matrix.android.internal.session.room.typing.DefaultTypingService import im.vector.matrix.android.internal.session.room.uploads.DefaultUploadsService @@ -52,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona private val reportingServiceFactory: DefaultReportingService.Factory, private val readServiceFactory: DefaultReadService.Factory, private val typingServiceFactory: DefaultTypingService.Factory, + private val tagsServiceFactory: DefaultTagsService.Factory, private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory, private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory, @@ -72,6 +74,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona reportingService = reportingServiceFactory.create(roomId), readService = readServiceFactory.create(roomId), typingService = typingServiceFactory.create(roomId), + tagsService = tagsServiceFactory.create(roomId), cryptoService = cryptoService, relationService = relationServiceFactory.create(roomId), roomMembersService = membershipServiceFactory.create(roomId), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 001ce120c8..0572a37506 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -34,6 +34,8 @@ import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTas import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyProtocolsTask import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask +import im.vector.matrix.android.internal.session.room.membership.admin.DefaultMembershipAdminTask +import im.vector.matrix.android.internal.session.room.membership.admin.MembershipAdminTask import im.vector.matrix.android.internal.session.room.membership.joining.DefaultInviteTask import im.vector.matrix.android.internal.session.room.membership.joining.DefaultJoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask @@ -56,6 +58,10 @@ import im.vector.matrix.android.internal.session.room.reporting.DefaultReportCon import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask +import im.vector.matrix.android.internal.session.room.tags.AddTagToRoomTask +import im.vector.matrix.android.internal.session.room.tags.DefaultAddTagToRoomTask +import im.vector.matrix.android.internal.session.room.tags.DefaultDeleteTagFromRoomTask +import im.vector.matrix.android.internal.session.room.tags.DeleteTagFromRoomTask import im.vector.matrix.android.internal.session.room.timeline.DefaultFetchNextTokenAndPaginateTask import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask @@ -66,6 +72,9 @@ import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTa import im.vector.matrix.android.internal.session.room.typing.SendTypingTask import im.vector.matrix.android.internal.session.room.uploads.DefaultGetUploadsTask import im.vector.matrix.android.internal.session.room.uploads.GetUploadsTask +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.commonmark.renderer.text.TextContentRenderer import retrofit2.Retrofit @Module @@ -79,6 +88,28 @@ internal abstract class RoomModule { fun providesRoomAPI(retrofit: Retrofit): RoomAPI { return retrofit.create(RoomAPI::class.java) } + + @Provides + @JvmStatic + fun providesParser(): Parser { + return Parser.builder().build() + } + + @Provides + @JvmStatic + fun providesHtmlRenderer(): HtmlRenderer { + return HtmlRenderer + .builder() + .build() + } + + @Provides + @JvmStatic + fun providesTextContentRenderer(): TextContentRenderer { + return TextContentRenderer + .builder() + .build() + } } @Binds @@ -117,6 +148,9 @@ internal abstract class RoomModule { @Binds abstract fun bindLeaveRoomTask(task: DefaultLeaveRoomTask): LeaveRoomTask + @Binds + abstract fun bindMembershipAdminTask(task: DefaultMembershipAdminTask): MembershipAdminTask + @Binds abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask @@ -161,4 +195,10 @@ internal abstract class RoomModule { @Binds abstract fun bindGetUploadsTask(task: DefaultGetUploadsTask): GetUploadsTask + + @Binds + abstract fun bindAddTagToRoomTask(task: DefaultAddTagToRoomTask): AddTagToRoomTask + + @Binds + abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index d17614ca4e..1167b3c26c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.query.process +import im.vector.matrix.android.internal.session.room.membership.admin.MembershipAdminTask import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask @@ -48,6 +49,7 @@ internal class DefaultMembershipService @AssistedInject constructor( private val inviteTask: InviteTask, private val joinTask: JoinRoomTask, private val leaveRoomTask: LeaveRoomTask, + private val membershipAdminTask: MembershipAdminTask, @UserId private val userId: String ) : MembershipService { @@ -113,6 +115,33 @@ internal class DefaultMembershipService @AssistedInject constructor( } } + override fun ban(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + val params = MembershipAdminTask.Params(MembershipAdminTask.Type.BAN, roomId, userId, reason) + return membershipAdminTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun unban(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + val params = MembershipAdminTask.Params(MembershipAdminTask.Type.UNBAN, roomId, userId, reason) + return membershipAdminTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun kick(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + val params = MembershipAdminTask.Params(MembershipAdminTask.Type.KICK, roomId, userId, reason) + return membershipAdminTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + override fun invite(userId: String, reason: String?, callback: MatrixCallback): Cancelable { val params = InviteTask.Params(roomId, userId, reason) return inviteTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt index 9bd97cec10..d7d578b635 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.room.membership 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.Membership import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.internal.session.user.UserEntityFactory import io.realm.Realm @@ -35,7 +34,7 @@ internal class RoomMemberEventHandler @Inject constructor() { val userId = event.stateKey ?: return false val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) realm.insertOrUpdate(roomMemberEntity) - if (roomMember.membership in Membership.activeMemberships()) { + if (roomMember.membership.isActive()) { val userEntity = UserEntityFactory.create(userId, roomMember) realm.insertOrUpdate(userEntity) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/admin/MembershipAdminTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/admin/MembershipAdminTask.kt new file mode 100644 index 0000000000..676a7de787 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/admin/MembershipAdminTask.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.membership.admin + +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface MembershipAdminTask : Task { + + enum class Type { + BAN, + UNBAN, + KICK + } + + data class Params( + val type: Type, + val roomId: String, + val userId: String, + val reason: String? + ) +} + +internal class DefaultMembershipAdminTask @Inject constructor(private val roomAPI: RoomAPI) : MembershipAdminTask { + + override suspend fun execute(params: MembershipAdminTask.Params) { + val userIdAndReason = UserIdAndReason(params.userId, params.reason) + executeRequest(null) { + apiCall = when (params.type) { + MembershipAdminTask.Type.BAN -> roomAPI.ban(params.roomId, userIdAndReason) + MembershipAdminTask.Type.UNBAN -> roomAPI.unban(params.roomId, userIdAndReason) + MembershipAdminTask.Type.KICK -> roomAPI.kick(params.roomId, userIdAndReason) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/admin/UserIdAndReason.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/admin/UserIdAndReason.kt new file mode 100644 index 0000000000..931602a79e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/admin/UserIdAndReason.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.room.membership.admin + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UserIdAndReason( + @Json(name = "user_id") val userId: String, + @Json(name = "reason") val reason: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 9c8723af05..d60e652e12 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -33,6 +33,7 @@ import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.CancelableBag +import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.content.UploadContentWorker @@ -67,6 +68,12 @@ internal class DefaultSendService @AssistedInject constructor( private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() + override fun sendEvent(eventType: String, content: JsonDict?): Cancelable { + return localEchoEventFactory.createEvent(roomId, eventType, content) + .also { createLocalEcho(it) } + .let { sendEvent(it) } + } + override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown) .also { createLocalEcho(it) } 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 82d393e79a..08dc4e80d8 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 @@ -23,6 +23,7 @@ import androidx.exifinterface.media.ExifInterface import im.vector.matrix.android.R import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.matrix.android.api.session.events.model.Content 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.LocalEcho @@ -57,14 +58,11 @@ import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent 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.internal.di.UserId -import im.vector.matrix.android.internal.extensions.subStringBetween import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.StringProvider import kotlinx.coroutines.launch -import org.commonmark.parser.Parser -import org.commonmark.renderer.html.HtmlRenderer import javax.inject.Inject /** @@ -80,41 +78,23 @@ internal class LocalEchoEventFactory @Inject constructor( private val context: Context, @UserId private val userId: String, private val stringProvider: StringProvider, + private val markdownParser: MarkdownParser, private val textPillsUtils: TextPillsUtils, private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository ) { - // TODO Inject - private val parser = Parser.builder().build() - - // TODO Inject - private val renderer = HtmlRenderer.builder().build() - fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event { if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) { return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType) } val content = MessageTextContent(msgType = msgType, body = text.toString()) - return createEvent(roomId, content) + return createMessageEvent(roomId, content) } private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { if (autoMarkdown) { - val source = textPillsUtils.processSpecialSpansToMarkdown(text) - ?: text.toString() - val document = parser.parse(source) - val htmlText = renderer.render(document) - - // Cleanup extra paragraph - val cleanHtmlText = if (htmlText.startsWith("

") && htmlText.endsWith("

\n")) { - htmlText.subStringBetween("

", "

\n") - } else { - htmlText - } - - if (isFormattedTextPertinent(source, cleanHtmlText)) { - return TextContent(text.toString(), cleanHtmlText) - } + val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() + return markdownParser.parse(source) } else { // Try to detect pills textPillsUtils.processSpecialSpansToHtml(text)?.let { @@ -125,11 +105,8 @@ internal class LocalEchoEventFactory @Inject constructor( return TextContent(text.toString()) } - private fun isFormattedTextPertinent(text: String, htmlText: String?) = - text != htmlText && htmlText != "

${text.trim()}

\n" - fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event { - return createEvent(roomId, textContent.toMessageTextContent(msgType)) + return createMessageEvent(roomId, textContent.toMessageTextContent(msgType)) } fun createReplaceTextEvent(roomId: String, @@ -138,7 +115,7 @@ internal class LocalEchoEventFactory @Inject constructor( newBodyAutoMarkdown: Boolean, msgType: String, compatibilityText: String): Event { - return createEvent(roomId, + return createMessageEvent(roomId, MessageTextContent( msgType = msgType, body = compatibilityText, @@ -153,7 +130,7 @@ internal class LocalEchoEventFactory @Inject constructor( pollEventId: String, optionIndex: Int, optionLabel: String): Event { - return createEvent(roomId, + return createMessageEvent(roomId, MessagePollResponseContent( body = optionLabel, relatesTo = RelationDefaultContent( @@ -175,7 +152,7 @@ internal class LocalEchoEventFactory @Inject constructor( append(it.value) } } - return createEvent( + return createMessageEvent( roomId, MessageOptionsContent( body = compatLabel, @@ -211,7 +188,7 @@ internal class LocalEchoEventFactory @Inject constructor( // val replyFallback = buildReplyFallback(body, originalEvent.root.senderId ?: "", newBodyText) - return createEvent(roomId, + return createMessageEvent(roomId, MessageTextContent( msgType = msgType, body = compatibilityText, @@ -280,7 +257,7 @@ internal class LocalEchoEventFactory @Inject constructor( ), url = attachment.queryUri.toString() ) - return createEvent(roomId, content) + return createMessageEvent(roomId, content) } private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { @@ -316,7 +293,7 @@ internal class LocalEchoEventFactory @Inject constructor( ), url = attachment.queryUri.toString() ) - return createEvent(roomId, content) + return createMessageEvent(roomId, content) } private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { @@ -329,7 +306,7 @@ internal class LocalEchoEventFactory @Inject constructor( ), url = attachment.queryUri.toString() ) - return createEvent(roomId, content) + return createMessageEvent(roomId, content) } private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event { @@ -342,18 +319,22 @@ internal class LocalEchoEventFactory @Inject constructor( ), url = attachment.queryUri.toString() ) - return createEvent(roomId, content) + return createMessageEvent(roomId, content) } - private fun createEvent(roomId: String, content: Any? = null): Event { + private fun createMessageEvent(roomId: String, content: MessageContent? = null): Event { + return createEvent(roomId, EventType.MESSAGE, content.toContent()) + } + + fun createEvent(roomId: String, type: String, content: Content?): Event { val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.MESSAGE, - content = content.toContent(), + type = type, + content = content, unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -410,7 +391,7 @@ internal class LocalEchoEventFactory @Inject constructor( formattedBody = replyFormatted, relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId)) ) - return createEvent(roomId, content) + return createMessageEvent(roomId, content) } private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MarkdownParser.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MarkdownParser.kt new file mode 100644 index 0000000000..92de583de7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/MarkdownParser.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.send + +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import org.commonmark.renderer.text.TextContentRenderer +import javax.inject.Inject + +/** + * This class convert a text to an html text + * This class is tested by [MarkdownParserTest]. + * If any change is required, please add a test covering the problem and make sure all the tests are still passing. + */ +internal class MarkdownParser @Inject constructor( + private val parser: Parser, + private val htmlRenderer: HtmlRenderer, + private val textContentRenderer: TextContentRenderer +) { + + private val mdSpecialChars = "[`_\\-\\*>\\.\\[\\]#~]".toRegex() + + fun parse(text: String): TextContent { + // If no special char are detected, just return plain text + if (text.contains(mdSpecialChars).not()) { + return TextContent(text.toString()) + } + + val document = parser.parse(text) + val htmlText = htmlRenderer.render(document) + + // Cleanup extra paragraph + val cleanHtmlText = if (htmlText.lastIndexOf("

") == 0) { + htmlText.removeSurrounding("

", "

\n") + } else { + htmlText + } + + return if (isFormattedTextPertinent(text, cleanHtmlText)) { + // According to https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes: + // The plain text version of the HTML should be provided in the body. + val plainText = textContentRenderer.render(document) + TextContent(plainText, cleanHtmlText.postTreatment()) + } else { + TextContent(text.toString()) + } + } + + private fun isFormattedTextPertinent(text: String, htmlText: String?) = + text != htmlText && htmlText != "

${text.trim()}

\n" + + /** + * The parser makes some mistakes, so deal with it here + */ + private fun String.postTreatment(): String { + return this + // Remove extra space before and after the content + .trim() + // There is no need to include new line in an html-like source + .replace("\n", "") + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index e831ffbb38..6646f08c2d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -17,26 +17,21 @@ package im.vector.matrix.android.internal.session.room.state 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.query.QueryStringValue import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.state.StateService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.JsonDict 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.CurrentStateEventEntity -import im.vector.matrix.android.internal.database.query.getOrNull -import im.vector.matrix.android.internal.database.query.whereStateKey import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import io.realm.Realm internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, - private val monarchy: Monarchy, + private val stateEventDataSource: StateEventDataSource, private val taskExecutor: TaskExecutor, private val sendStateTask: SendStateTask ) : StateService { @@ -46,33 +41,47 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private fun create(roomId: String): StateService } - override fun getStateEvent(eventType: String, stateKey: String): Event? { - return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - CurrentStateEventEntity.getOrNull(realm, roomId, type = eventType, stateKey = stateKey)?.root?.asDomain() - } + override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? { + return stateEventDataSource.getStateEvent(roomId, eventType, stateKey) } - override fun getStateEventLive(eventType: String, stateKey: String): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm -> CurrentStateEventEntity.whereStateKey(realm, roomId, type = eventType, stateKey = "") }, - { it.root?.asDomain() } + override fun getStateEventLive(eventType: String, stateKey: QueryStringValue): LiveData> { + return stateEventDataSource.getStateEventLive(roomId, eventType, stateKey) + } + + override fun getStateEvents(eventTypes: Set, stateKey: QueryStringValue): List { + return stateEventDataSource.getStateEvents(roomId, eventTypes, stateKey) + } + + override fun getStateEventsLive(eventTypes: Set, stateKey: QueryStringValue): LiveData> { + return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey) + } + + override fun sendStateEvent( + eventType: String, + stateKey: String?, + body: JsonDict, + callback: MatrixCallback + ): Cancelable { + val params = SendStateTask.Params( + roomId = roomId, + stateKey = stateKey, + eventType = eventType, + body = body ) - return Transformations.map(liveData) { results -> - results.firstOrNull().toOptional() - } - } - - override fun updateTopic(topic: String, callback: MatrixCallback) { - val params = SendStateTask.Params(roomId, - EventType.STATE_ROOM_TOPIC, - mapOf( - "topic" to topic - )) - - sendStateTask + return sendStateTask .configureWith(params) { this.callback = callback } .executeBy(taskExecutor) } + + override fun updateTopic(topic: String, callback: MatrixCallback): Cancelable { + return sendStateEvent( + eventType = EventType.STATE_ROOM_TOPIC, + body = mapOf("topic" to topic), + callback = callback, + stateKey = null + ) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt index b0d583c6d1..5c1cddc2e7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/SendStateTask.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room.state +import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task @@ -25,8 +26,9 @@ import javax.inject.Inject internal interface SendStateTask : Task { data class Params( val roomId: String, + val stateKey: String?, val eventType: String, - val body: Map + val body: JsonDict ) } @@ -37,7 +39,20 @@ internal class DefaultSendStateTask @Inject constructor( override suspend fun execute(params: SendStateTask.Params) { return executeRequest(eventBus) { - apiCall = roomAPI.sendStateEvent(params.roomId, params.eventType, params.body) + apiCall = if (params.stateKey == null) { + roomAPI.sendStateEvent( + roomId = params.roomId, + stateEventType = params.eventType, + params = params.body + ) + } else { + roomAPI.sendStateEvent( + roomId = params.roomId, + stateEventType = params.eventType, + stateKey = params.stateKey, + params = params.body + ) + } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/StateEventDataSource.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/StateEventDataSource.kt new file mode 100644 index 0000000000..3f0e9d7dda --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/StateEventDataSource.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.state + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.events.model.Event +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.CurrentStateEventEntity +import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields +import im.vector.matrix.android.internal.query.process +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where +import javax.inject.Inject + +internal class StateEventDataSource @Inject constructor(private val monarchy: Monarchy) { + + fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + buildStateEventQuery(realm, roomId, setOf(eventType), stateKey).findFirst()?.root?.asDomain() + } + } + + fun getStateEventLive(roomId: String, eventType: String, stateKey: QueryStringValue): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> buildStateEventQuery(realm, roomId, setOf(eventType), stateKey) }, + { it.root?.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getStateEvents(roomId: String, eventTypes: Set, stateKey: QueryStringValue): List { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + buildStateEventQuery(realm, roomId, eventTypes, stateKey) + .findAll() + .mapNotNull { + it.root?.asDomain() + } + } + } + + fun getStateEventsLive(roomId: String, eventTypes: Set, stateKey: QueryStringValue): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> buildStateEventQuery(realm, roomId, eventTypes, stateKey) }, + { it.root?.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.filterNotNull() + } + } + + private fun buildStateEventQuery(realm: Realm, + roomId: String, + eventTypes: Set, + stateKey: QueryStringValue + ): RealmQuery { + return realm.where() + .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId) + .`in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray()) + .process(CurrentStateEventEntityFields.STATE_KEY, stateKey) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/AddTagToRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/AddTagToRoomTask.kt new file mode 100644 index 0000000000..0d1832050c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/AddTagToRoomTask.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.tags + +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface AddTagToRoomTask : Task { + + data class Params( + val roomId: String, + val tag: String, + val order: Double? + ) +} + +internal class DefaultAddTagToRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus +) : AddTagToRoomTask { + + override suspend fun execute(params: AddTagToRoomTask.Params) { + executeRequest(eventBus) { + apiCall = roomAPI.putTag( + userId = userId, + roomId = params.roomId, + tag = params.tag, + body = TagBody( + order = params.order + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/DefaultTagsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/DefaultTagsService.kt new file mode 100644 index 0000000000..9df2f0593b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/DefaultTagsService.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.tags + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.room.tags.TagsService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith + +internal class DefaultTagsService @AssistedInject constructor( + @Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val addTagToRoomTask: AddTagToRoomTask, + private val deleteTagFromRoomTask: DeleteTagFromRoomTask +) : TagsService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): TagsService + } + + override fun addTag(tag: String, order: Double?, callback: MatrixCallback): Cancelable { + val params = AddTagToRoomTask.Params(roomId, tag, order) + return addTagToRoomTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun deleteTag(tag: String, callback: MatrixCallback): Cancelable { + val params = DeleteTagFromRoomTask.Params(roomId, tag) + return deleteTagFromRoomTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/DeleteTagFromRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/DeleteTagFromRoomTask.kt new file mode 100644 index 0000000000..2907df4953 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/DeleteTagFromRoomTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.tags + +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeleteTagFromRoomTask : Task { + + data class Params( + val roomId: String, + val tag: String + ) +} + +internal class DefaultDeleteTagFromRoomTask @Inject constructor( + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus +) : DeleteTagFromRoomTask { + + override suspend fun execute(params: DeleteTagFromRoomTask.Params) { + executeRequest(eventBus) { + apiCall = roomAPI.deleteTag( + userId = userId, + roomId = params.roomId, + tag = params.tag + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/TagBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/TagBody.kt new file mode 100644 index 0000000000..d82d37757f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/tags/TagBody.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.tags + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class TagBody( + /** + * A number in a range [0,1] describing a relative position of the room under the given tag. + */ + @Json(name = "order") + val order: Double? +) 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 95a8581c2b..9b66d0c4c0 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 @@ -775,6 +775,9 @@ internal class DefaultTimeline( if (settings.filterTypes) { `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } + if (settings.filterUseless) { + not().equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true) + } if (settings.filterEdits) { not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 72e99701cd..ddfa7e91fe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -154,6 +154,11 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) needOr = true } + if (settings.filterUseless) { + if (needOr) or() + equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.IS_USELESS}", true) + needOr = true + } if (settings.filterEdits) { if (needOr) or() like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index cca0af7feb..6db35f084c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -49,7 +49,7 @@ internal class DefaultSignOutTask @Inject constructor( apiCall = signOutAPI.signOut() } } catch (throwable: Throwable) { - // Maybe due to https://github.com/matrix-org/synapse/issues/5755 + // Maybe due to https://github.com/matrix-org/synapse/issues/5756 if (throwable is Failure.ServerError && throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ && throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) { 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 a910944fbf..8682a84763 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 @@ -35,6 +35,7 @@ import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastForwardChunkOfRoom @@ -42,6 +43,7 @@ import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater @@ -67,6 +69,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val roomFullyReadHandler: RoomFullyReadHandler, private val cryptoService: DefaultCryptoService, private val roomMemberEventHandler: RoomMemberEventHandler, + @UserId private val userId: String, private val eventBus: EventBus) { sealed class HandlingStrategy { @@ -208,9 +211,37 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomId: String, roomSync: RoomSync): RoomEntity { val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) - roomEntity.membership = Membership.LEAVE + for (event in roomSync.state?.events.orEmpty()) { + if (event.eventId == null || event.stateKey == null) { + continue + } + val eventEntity = event.toEntity(roomId, SendState.SYNCED).copyToRealmOrIgnore(realm) + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + roomMemberEventHandler.handle(realm, roomId, event) + } + for (event in roomSync.timeline?.events.orEmpty()) { + if (event.eventId == null || event.senderId == null) { + continue + } + val eventEntity = event.toEntity(roomId, SendState.SYNCED).copyToRealmOrIgnore(realm) + if (event.stateKey != null) { + CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { + eventId = event.eventId + root = eventEntity + } + if (event.type == EventType.STATE_ROOM_MEMBER) { + roomMemberEventHandler.handle(realm, roomEntity.roomId, event) + } + } + } + val leftMember = RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst() + val membership = leftMember?.membership ?: Membership.LEAVE + roomEntity.membership = membership roomEntity.chunks.deleteAllFromRealm() - roomSummaryUpdater.update(realm, roomId, Membership.LEAVE, roomSync.summary, roomSync.unreadNotifications) + roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications) return roomEntity } @@ -241,7 +272,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle eventIds.add(event.eventId) val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm) - if (event.isStateEvent() && event.stateKey != null) { + if (event.stateKey != null) { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = event.eventId root = eventEntity diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt index c2e36604e3..d758110e09 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt @@ -30,6 +30,8 @@ abstract class UserAccountData : AccountDataContent { const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls" const val TYPE_WIDGETS = "m.widgets" const val TYPE_PUSH_RULES = "m.push_rules" + const val TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning" + const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets" const val TYPE_IDENTITY_SERVER = "m.identity_server" const val TYPE_ACCEPTED_TERMS = "m.accepted_terms" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAllowedWidgets.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAllowedWidgets.kt new file mode 100644 index 0000000000..43f3b3b32a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAllowedWidgets.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.session.integrationmanager.AllowedWidgetsContent + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataAllowedWidgets( + @Json(name = "type") override val type: String = TYPE_ALLOWED_WIDGETS, + @Json(name = "content") val content: AllowedWidgetsContent +) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataEvent.kt index a4ba0fc91a..65d3ce1ce7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataEvent.kt @@ -18,9 +18,10 @@ 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 @JsonClass(generateAdapter = true) data class UserAccountDataEvent( @Json(name = "type") override val type: String, - @Json(name = "content") val content: Map + @Json(name = "content") val content: JsonDict ) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt new file mode 100644 index 0000000000..a47bb761cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.session.integrationmanager.IntegrationProvisioningContent + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataIntegrationProvisioning( + @Json(name = "type") override val type: String = TYPE_INTEGRATION_PROVISIONING, + @Json(name = "content") val content: IntegrationProvisioningContent +) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataWidgets.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataWidgets.kt new file mode 100644 index 0000000000..5c13b4d5fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataWidgets.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Event + +/* +"m.widgets":{ + "stickerpicker_@rxl881:matrix.org_1514573757015":{ + "content":{ + "creatorUserId":"@rxl881:matrix.org", + "data":{ + "..." + }, + "id":"stickerpicker_@rxl881:matrix.org_1514573757015", + "name":"Stickerpicker", + "type":"m.stickerpicker", + "url":"https://...", + "waitForIframeLoad":true + }, + "sender":"@rxl881:matrix.org" + "state_key":"stickerpicker_@rxl881:matrix.org_1514573757015", + "type":"m.widget" + }, +{ + "..." + } +} + */ +@JsonClass(generateAdapter = true) +internal data class UserAccountDataWidgets( + @Json(name = "type") override val type: String = TYPE_WIDGETS, + @Json(name = "content") val content: Map +) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt index 6d5e597da8..9111c5d5f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt @@ -28,14 +28,15 @@ import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI import im.vector.matrix.android.internal.session.identity.IdentityRegisterTask -import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource 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.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.ensureTrailingSlash import okhttp3.OkHttpClient import javax.inject.Inject @@ -55,17 +56,10 @@ internal class DefaultTermsService @Inject constructor( baseUrl: String, callback: MatrixCallback): Cancelable { return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - val sep = if (baseUrl.endsWith("/")) "" else "/" - - val url = when (serviceType) { - TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}" - TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}" - } - + val url = buildUrl(baseUrl, serviceType) val termsResponse = executeRequest(null) { apiCall = termsAPI.getTerms("${url}terms") } - GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData()) } } @@ -76,13 +70,7 @@ internal class DefaultTermsService @Inject constructor( token: String?, callback: MatrixCallback): Cancelable { return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - val sep = if (baseUrl.endsWith("/")) "" else "/" - - val url = when (serviceType) { - TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}" - TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}" - } - + val url = buildUrl(baseUrl, serviceType) val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl) executeRequest(null) { @@ -112,6 +100,14 @@ internal class DefaultTermsService @Inject constructor( return token.token } + private fun buildUrl(baseUrl: String, serviceType: TermsService.ServiceType): String { + val servicePath = when (serviceType) { + TermsService.ServiceType.IntegrationManager -> NetworkConstants.URI_INTEGRATION_MANAGER_PATH + TermsService.ServiceType.IdentityService -> NetworkConstants.URI_IDENTITY_PATH_V2 + } + return "${baseUrl.ensureTrailingSlash()}$servicePath" + } + private fun getAlreadyAcceptedTermUrlsFromAccountData(): Set { return accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ACCEPTED_TERMS) ?.content 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 7cd2f1b743..5cad504555 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 @@ -17,101 +17,41 @@ package im.vector.matrix.android.internal.session.user import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations -import androidx.paging.DataSource -import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.model.User 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 -import im.vector.matrix.android.internal.util.fetchCopied import javax.inject.Inject -internal class DefaultUserService @Inject constructor(private val monarchy: Monarchy, +internal class DefaultUserService @Inject constructor(private val userDataSource: UserDataSource, 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) - .isNotEmpty(UserEntityFields.USER_ID) - .sort(UserEntityFields.DISPLAY_NAME) - } - } - - private val domainDataSourceFactory: DataSource.Factory by lazy { - realmDataSourceFactory.map { - it.asDomain() - } - } - - private val livePagedListBuilder: LivePagedListBuilder by lazy { - LivePagedListBuilder(domainDataSourceFactory, PagedList.Config.Builder().setPageSize(100).setEnablePlaceholders(false).build()) - } override fun getUser(userId: String): User? { - val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } - ?: return null - - return userEntity.asDomain() + return userDataSource.getUser(userId) } override fun getUserLive(userId: String): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { UserEntity.where(it, userId) }, - { it.asDomain() } - ) - return Transformations.map(liveData) { results -> - results.firstOrNull().toOptional() - } + return userDataSource.getUserLive(userId) } override fun getUsersLive(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm -> - realm.where(UserEntity::class.java) - .isNotEmpty(UserEntityFields.USER_ID) - .sort(UserEntityFields.DISPLAY_NAME) - }, - { it.asDomain() } - ) + return userDataSource.getUsersLive() } override fun getPagedUsersLive(filter: String?, excludedUserIds: Set?): LiveData> { - realmDataSourceFactory.updateQuery { realm -> - val query = realm.where(UserEntity::class.java) - if (filter.isNullOrEmpty()) { - query.isNotEmpty(UserEntityFields.USER_ID) - } else { - query - .beginGroup() - .contains(UserEntityFields.DISPLAY_NAME, filter) - .or() - .contains(UserEntityFields.USER_ID, filter) - .endGroup() - } - excludedUserIds - ?.takeIf { it.isNotEmpty() } - ?.let { - query.not().`in`(UserEntityFields.USER_ID, it.toTypedArray()) - } - query.sort(UserEntityFields.DISPLAY_NAME) - } - return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) + return userDataSource.getPagedUsersLive(filter, excludedUserIds) + } + + override fun getIgnoredUsersLive(): LiveData> { + return userDataSource.getIgnoredUsersLive() } override fun searchUsersDirectory(search: String, @@ -126,17 +66,6 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona .executeBy(taskExecutor) } - override fun getIgnoredUsersLive(): 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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserDataSource.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserDataSource.kt new file mode 100644 index 0000000000..3fcd0bbb74 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserDataSource.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.user + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import androidx.paging.DataSource +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.user.model.User +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.util.fetchCopied +import javax.inject.Inject + +internal class UserDataSource @Inject constructor(private val monarchy: Monarchy) { + + private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory by lazy { + monarchy.createDataSourceFactory { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + } + } + + private val domainDataSourceFactory: DataSource.Factory by lazy { + realmDataSourceFactory.map { + it.asDomain() + } + } + + private val livePagedListBuilder: LivePagedListBuilder by lazy { + LivePagedListBuilder(domainDataSourceFactory, PagedList.Config.Builder().setPageSize(100).setEnablePlaceholders(false).build()) + } + + fun getUser(userId: String): User? { + val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } + ?: return null + + return userEntity.asDomain() + } + + fun getUserLive(userId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { UserEntity.where(it, userId) }, + { it.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getUsersLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm -> + realm.where(UserEntity::class.java) + .isNotEmpty(UserEntityFields.USER_ID) + .sort(UserEntityFields.DISPLAY_NAME) + }, + { it.asDomain() } + ) + } + + fun getPagedUsersLive(filter: String?, excludedUserIds: Set?): LiveData> { + realmDataSourceFactory.updateQuery { realm -> + val query = realm.where(UserEntity::class.java) + if (filter.isNullOrEmpty()) { + query.isNotEmpty(UserEntityFields.USER_ID) + } else { + query + .beginGroup() + .contains(UserEntityFields.DISPLAY_NAME, filter) + .or() + .contains(UserEntityFields.USER_ID, filter) + .endGroup() + } + excludedUserIds + ?.takeIf { it.isNotEmpty() } + ?.let { + query.not().`in`(UserEntityFields.USER_ID, it.toTypedArray()) + } + query.sort(UserEntityFields.DISPLAY_NAME) + } + return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) + } + + fun getIgnoredUsersLive(): 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) } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataDataSource.kt similarity index 93% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataDataSource.kt index 37b0da9101..056d9c41f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataDataSource.kt @@ -14,13 +14,14 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.identity.todelete +package im.vector.matrix.android.internal.session.user.accountdata import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.database.mapper.AccountDataMapper import im.vector.matrix.android.internal.database.model.UserAccountDataEntity import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent @@ -28,7 +29,6 @@ import io.realm.Realm import io.realm.RealmQuery import javax.inject.Inject -// There will be a duplicated class when Integration manager will be merged, so delete this one internal class AccountDataDataSource @Inject constructor(private val monarchy: Monarchy, private val accountDataMapper: AccountDataMapper) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt index 31abc800c6..9787f47844 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt @@ -17,18 +17,12 @@ package im.vector.matrix.android.internal.session.user.accountdata import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.accountdata.AccountDataService import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE import im.vector.matrix.android.api.util.Optional -import im.vector.matrix.android.api.util.toOptional -import im.vector.matrix.android.internal.database.model.UserAccountDataEntity -import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields -import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.session.sync.UserAccountDataSyncHandler import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.internal.task.TaskExecutor @@ -39,54 +33,24 @@ internal class DefaultAccountDataService @Inject constructor( private val monarchy: Monarchy, private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, + private val accountDataDataSource: AccountDataDataSource, private val taskExecutor: TaskExecutor ) : AccountDataService { - private val moshi = MoshiProvider.providesMoshi() - private val adapter = moshi.adapter>(JSON_DICT_PARAMETERIZED_TYPE) - override fun getAccountDataEvent(type: String): UserAccountDataEvent? { - return getAccountDataEvents(setOf(type)).firstOrNull() + return accountDataDataSource.getAccountDataEvent(type) } override fun getLiveAccountDataEvent(type: String): LiveData> { - return Transformations.map(getLiveAccountDataEvents(setOf(type))) { - it.firstOrNull()?.toOptional() - } + return accountDataDataSource.getLiveAccountDataEvent(type) } override fun getAccountDataEvents(types: Set): List { - return monarchy.fetchAllCopiedSync { realm -> - realm.where(UserAccountDataEntity::class.java) - .apply { - if (types.isNotEmpty()) { - `in`(UserAccountDataEntityFields.TYPE, types.toTypedArray()) - } - } - }.mapNotNull { entity -> - entity.type?.let { type -> - UserAccountDataEvent( - type = type, - content = entity.contentStr?.let { adapter.fromJson(it) }.orEmpty() - ) - } - } + return accountDataDataSource.getAccountDataEvents(types) } override fun getLiveAccountDataEvents(types: Set): LiveData> { - return monarchy.findAllMappedWithChanges({ realm -> - realm.where(UserAccountDataEntity::class.java) - .apply { - if (types.isNotEmpty()) { - `in`(UserAccountDataEntityFields.TYPE, types.toTypedArray()) - } - } - }, { entity -> - UserAccountDataEvent( - type = entity.type ?: "", - content = entity.contentStr?.let { adapter.fromJson(it) }.orEmpty() - ) - }) + return accountDataDataSource.getLiveAccountDataEvents(types) } override fun updateAccountData(type: String, content: Content, callback: MatrixCallback?): Cancelable { 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 07242984b5..8db2ad9781 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,6 +18,8 @@ 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.integrationmanager.AllowedWidgetsContent +import im.vector.matrix.android.internal.session.integrationmanager.IntegrationProvisioningContent import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent @@ -70,6 +72,22 @@ internal interface UpdateUserAccountDataTask : Task { + + data class Params( + val roomId: String, + val widgetId: String, + val content: Content + ) +} + +internal class DefaultCreateWidgetTask @Inject constructor(private val monarchy: Monarchy, + private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val eventBus: EventBus) : CreateWidgetTask { + + override suspend fun execute(params: CreateWidgetTask.Params) { + executeRequest(eventBus) { + apiCall = roomAPI.sendStateEvent( + roomId = params.roomId, + stateEventType = EventType.STATE_ROOM_WIDGET_LEGACY, + stateKey = params.widgetId, + params = params.content + ) + } + awaitNotEmptyResult(monarchy.realmConfiguration, 30_000L) { + CurrentStateEventEntity + .whereStateKey(it, params.roomId, type = EventType.STATE_ROOM_WIDGET_LEGACY, stateKey = params.widgetId) + .and() + .equalTo(CurrentStateEventEntityFields.ROOT.SENDER, userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/DefaultWidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/DefaultWidgetPostAPIMediator.kt new file mode 100644 index 0000000000..345ef39edd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/DefaultWidgetPostAPIMediator.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets + +import android.os.Build +import android.webkit.JavascriptInterface +import android.webkit.WebView +import com.squareup.moshi.Moshi +import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator +import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.util.createUIHandler +import timber.log.Timber +import java.lang.reflect.Type +import java.util.HashMap +import javax.inject.Inject + +internal class DefaultWidgetPostAPIMediator @Inject constructor(private val moshi: Moshi, + private val widgetPostMessageAPIProvider: WidgetPostMessageAPIProvider) + : WidgetPostAPIMediator { + + private val jsonAdapter = moshi.adapter(JSON_DICT_PARAMETERIZED_TYPE) + + private var handler: WidgetPostAPIMediator.Handler? = null + private var webView: WebView? = null + + private val uiHandler = createUIHandler() + + override fun setWebView(webView: WebView) { + this.webView = webView + webView.addJavascriptInterface(this, "Android") + } + + override fun clearWebView() { + webView?.removeJavascriptInterface("Android") + webView = null + } + + override fun setHandler(handler: WidgetPostAPIMediator.Handler?) { + this.handler = handler + } + + override fun injectAPI() { + val js = widgetPostMessageAPIProvider.get() + if (js != null) { + uiHandler.post { + webView?.loadUrl("javascript:$js") + } + } + } + + @JavascriptInterface + fun onWidgetEvent(jsonEventData: String) { + Timber.d("BRIDGE onWidgetEvent : $jsonEventData") + try { + val dataAsDict = jsonAdapter.fromJson(jsonEventData) + @Suppress("UNCHECKED_CAST") + val eventData = (dataAsDict?.get("event.data") as? JsonDict) ?: return + onWidgetMessage(eventData) + } catch (e: Exception) { + Timber.e(e, "## onWidgetEvent() failed") + } + } + + private fun onWidgetMessage(eventData: JsonDict) { + try { + if (handler?.handleWidgetRequest(eventData) == false) { + sendError("", eventData) + } + } catch (e: Exception) { + Timber.e(e, "## onWidgetMessage() : failed") + sendError("", eventData) + } + } + + /* + * ********************************************************************************************* + * Message sending methods + * ********************************************************************************************* + */ + + /** + * Send a boolean response + * + * @param response the response + * @param eventData the modular data + */ + override fun sendBoolResponse(response: Boolean, eventData: JsonDict) { + val jsString = if (response) "true" else "false" + sendResponse(jsString, eventData) + } + + /** + * Send an integer response + * + * @param response the response + * @param eventData the modular data + */ + override fun sendIntegerResponse(response: Int, eventData: JsonDict) { + sendResponse(response.toString() + "", eventData) + } + + /** + * Send an object response + * + * @param response the response + * @param eventData the modular data + */ + override fun sendObjectResponse(type: Type, response: T?, eventData: JsonDict) { + var jsString: String? = null + if (response != null) { + val objectAdapter = moshi.adapter(type) + try { + jsString = "JSON.parse('${objectAdapter.toJson(response)}')" + } catch (e: Exception) { + Timber.e(e, "## sendObjectResponse() : toJson failed ") + } + } + sendResponse(jsString ?: "null", eventData) + } + + /** + * Send success + * + * @param eventData the modular data + */ + override fun sendSuccess(eventData: JsonDict) { + val successResponse = mapOf("success" to true) + sendObjectResponse(Map::class.java, successResponse, eventData) + } + + /** + * Send an error + * + * @param message the error message + * @param eventData the modular data + */ + override fun sendError(message: String, eventData: JsonDict) { + Timber.e("## sendError() : eventData $eventData failed $message") + + // TODO: JS has an additional optional parameter: nestedError + val params = HashMap>() + val subMap = HashMap() + subMap["message"] = message + params["error"] = subMap + sendObjectResponse(Map::class.java, params, eventData) + } + + /** + * Send the response to the javascript + * + * @param jsString the response data + * @param eventData the modular data + */ + private fun sendResponse(jsString: String, eventData: JsonDict) = uiHandler.post { + try { + val functionLine = "sendResponseFromRiotAndroid('" + eventData["_id"] + "' , " + jsString + ");" + Timber.v("BRIDGE sendResponse: $functionLine") + // call the javascript method + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + webView?.loadUrl("javascript:$functionLine") + } else { + webView?.evaluateJavascript(functionLine, null) + } + } catch (e: Exception) { + Timber.e(e, "## sendResponse() failed ") + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/DefaultWidgetService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/DefaultWidgetService.kt new file mode 100644 index 0000000000..424168bc62 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/DefaultWidgetService.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets + +import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator +import im.vector.matrix.android.api.session.widgets.WidgetService +import im.vector.matrix.android.api.session.widgets.WidgetURLFormatter +import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.matrix.android.api.util.Cancelable +import javax.inject.Inject + +internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager, + private val widgetURLFormatter: WidgetURLFormatter, + private val widgetPostAPIMediator: WidgetPostAPIMediator) + : WidgetService { + + override fun getWidgetURLFormatter(): WidgetURLFormatter { + return widgetURLFormatter + } + + override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator { + return widgetPostAPIMediator + } + + override fun getRoomWidgets( + roomId: String, + widgetId: QueryStringValue, + widgetTypes: Set?, + excludedTypes: Set? + ): List { + return widgetManager.getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes) + } + + override fun getRoomWidgetsLive( + roomId: String, + widgetId: QueryStringValue, + widgetTypes: Set?, + excludedTypes: Set? + ): LiveData> { + return widgetManager.getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes) + } + + override fun getUserWidgetsLive( + widgetTypes: Set?, + excludedTypes: Set? + ): LiveData> { + return widgetManager.getUserWidgetsLive(widgetTypes, excludedTypes) + } + + override fun getUserWidgets( + widgetTypes: Set?, + excludedTypes: Set? + ): List { + return widgetManager.getUserWidgets(widgetTypes, excludedTypes) + } + + override fun createRoomWidget( + roomId: String, + widgetId: String, + content: Content, + callback: MatrixCallback + ): Cancelable { + return widgetManager.createRoomWidget(roomId, widgetId, content, callback) + } + + override fun destroyRoomWidget( + roomId: String, + widgetId: String, + callback: MatrixCallback + ): Cancelable { + return widgetManager.destroyRoomWidget(roomId, widgetId, callback) + } + + override fun hasPermissionsToHandleWidgets(roomId: String): Boolean { + return widgetManager.hasPermissionsToHandleWidgets(roomId) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/DefaultWidgetURLFormatter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/DefaultWidgetURLFormatter.kt new file mode 100644 index 0000000000..0cf99b6021 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/DefaultWidgetURLFormatter.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets + +import im.vector.matrix.android.api.MatrixConfiguration +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService +import im.vector.matrix.android.api.session.widgets.WidgetURLFormatter +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager +import im.vector.matrix.android.internal.session.widgets.token.GetScalarTokenTask +import java.net.URLEncoder +import javax.inject.Inject + +@SessionScope +internal class DefaultWidgetURLFormatter @Inject constructor(private val integrationManager: IntegrationManager, + private val getScalarTokenTask: GetScalarTokenTask, + private val matrixConfiguration: MatrixConfiguration +) : IntegrationManagerService.Listener, WidgetURLFormatter { + + private lateinit var currentConfig: IntegrationManagerConfig + private var whiteListedUrls: List = emptyList() + + fun start() { + setupWithConfiguration() + integrationManager.addListener(this) + } + + fun stop() { + integrationManager.removeListener(this) + } + + override fun onConfigurationChanged(configs: List) { + setupWithConfiguration() + } + + private fun setupWithConfiguration() { + val preferredConfig = integrationManager.getPreferredConfig() + if (!this::currentConfig.isInitialized || preferredConfig != currentConfig) { + currentConfig = preferredConfig + whiteListedUrls = if (matrixConfiguration.integrationWidgetUrls.isEmpty()) { + listOf(preferredConfig.restUrl) + } else { + matrixConfiguration.integrationWidgetUrls + } + } + } + + /** + * Takes care of fetching a scalar token if required and build the final url. + */ + override suspend fun format(baseUrl: String, params: Map, forceFetchScalarToken: Boolean, bypassWhitelist: Boolean): String { + return if (bypassWhitelist || isWhiteListed(baseUrl)) { + val taskParams = GetScalarTokenTask.Params(currentConfig.restUrl, forceFetchScalarToken) + val scalarToken = getScalarTokenTask.execute(taskParams) + buildString { + append(baseUrl) + appendParamToUrl("scalar_token", scalarToken) + appendParamsToUrl(params) + } + } else { + buildString { + append(baseUrl) + appendParamsToUrl(params) + } + } + } + + private fun isWhiteListed(url: String): Boolean { + val allowed: List = whiteListedUrls + for (allowedUrl in allowed) { + if (url.startsWith(allowedUrl)) { + return true + } + } + return false + } + + private fun StringBuilder.appendParamsToUrl(params: Map): StringBuilder { + params.forEach { (param, value) -> + appendParamToUrl(param, value) + } + return this + } + + private fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder { + if (contains("?")) { + append("&") + } else { + append("?") + } + + append(param) + append("=") + append(URLEncoder.encode(value, "utf-8")) + + return this + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/RegisterWidgetResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/RegisterWidgetResponse.kt new file mode 100644 index 0000000000..af62525822 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/RegisterWidgetResponse.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RegisterWidgetResponse( + @Json(name = "scalar_token") val scalarToken: String? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetDependenciesHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetDependenciesHolder.kt new file mode 100644 index 0000000000..42a8b7e8d2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetDependenciesHolder.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets + +import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager +import javax.inject.Inject + +internal class WidgetDependenciesHolder @Inject constructor(private val integrationManager: IntegrationManager, + private val widgetManager: WidgetManager, + private val widgetURLFormatter: DefaultWidgetURLFormatter) { + + fun start() { + integrationManager.start() + widgetManager.start() + widgetURLFormatter.start() + } + + fun stop() { + widgetURLFormatter.stop() + widgetManager.stop() + integrationManager.stop() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetManager.kt new file mode 100644 index 0000000000..8d13f53190 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetManager.kt @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.events.model.Content +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.integrationmanager.IntegrationManagerService +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper +import im.vector.matrix.android.api.session.widgets.WidgetManagementFailure +import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager +import im.vector.matrix.android.internal.session.room.state.StateEventDataSource +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource +import im.vector.matrix.android.internal.session.widgets.helper.WidgetFactory +import im.vector.matrix.android.internal.session.widgets.helper.extractWidgetSequence +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.launchToCallback +import java.util.HashMap +import javax.inject.Inject + +@SessionScope +internal class WidgetManager @Inject constructor(private val integrationManager: IntegrationManager, + private val accountDataDataSource: AccountDataDataSource, + private val stateEventDataSource: StateEventDataSource, + private val taskExecutor: TaskExecutor, + private val createWidgetTask: CreateWidgetTask, + private val widgetFactory: WidgetFactory, + @UserId private val userId: String) : IntegrationManagerService.Listener { + + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + fun start() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + integrationManager.addListener(this) + } + + fun stop() { + integrationManager.removeListener(this) + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + fun getRoomWidgetsLive( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): LiveData> { + // Get all im.vector.modular.widgets state events in the room + val liveWidgetEvents = stateEventDataSource.getStateEventsLive( + roomId = roomId, + eventTypes = setOf(EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET_LEGACY), + stateKey = widgetId + ) + return Transformations.map(liveWidgetEvents) { widgetEvents -> + widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes) + } + } + + fun getRoomWidgets( + roomId: String, + widgetId: QueryStringValue = QueryStringValue.NoCondition, + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): List { + // Get all im.vector.modular.widgets state events in the room + val widgetEvents: List = stateEventDataSource.getStateEvents( + roomId = roomId, + eventTypes = setOf(EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET_LEGACY), + stateKey = widgetId + ) + return widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes) + } + + private fun List.mapEventsToWidgets(widgetTypes: Set? = null, + excludedTypes: Set? = null): List { + val widgetEvents = this + // Widget id -> widget + val widgets: MutableMap = HashMap() + // Order widgetEvents with the last event first + // There can be several im.vector.modular.widgets state events for a same widget but + // only the last one must be considered. + val sortedWidgetEvents = widgetEvents.sortedByDescending { + it.originServerTs + } + // Create each widget from its latest im.vector.modular.widgets state event + for (widgetEvent in sortedWidgetEvents) { // Filter widget types if required + val widget = widgetFactory.create(widgetEvent) ?: continue + val widgetType = widget.widgetContent.type ?: continue + if (widgetTypes != null && !widgetTypes.contains(widgetType)) { + continue + } + if (excludedTypes != null && excludedTypes.contains(widgetType)) { + continue + } + if (!widgets.containsKey(widget.widgetId)) { + widgets[widget.widgetId] = widget + } + } + return widgets.values.toList() + } + + fun getUserWidgetsLive( + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): LiveData> { + val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountData.TYPE_WIDGETS) + return Transformations.map(widgetsAccountData) { + it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList() + } + } + + fun getUserWidgets( + widgetTypes: Set? = null, + excludedTypes: Set? = null + ): List { + val widgetsAccountData = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_WIDGETS) ?: return emptyList() + return widgetsAccountData.mapToWidgets(widgetTypes, excludedTypes) + } + + private fun UserAccountDataEvent.mapToWidgets(widgetTypes: Set? = null, + excludedTypes: Set? = null): List { + return extractWidgetSequence(widgetFactory) + .filter { + val widgetType = it.widgetContent.type ?: return@filter false + (widgetTypes == null || widgetTypes.contains(widgetType)) + && (excludedTypes == null || !excludedTypes.contains(widgetType)) + } + .toList() + } + + fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(callback = callback) { + if (!hasPermissionsToHandleWidgets(roomId)) { + throw WidgetManagementFailure.NotEnoughPower + } + val params = CreateWidgetTask.Params( + roomId = roomId, + widgetId = widgetId, + content = content + ) + createWidgetTask.execute(params) + try { + getRoomWidgets(roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.INSENSITIVE)).first() + } catch (failure: Throwable) { + throw WidgetManagementFailure.CreationFailed + } + } + } + + fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(callback = callback) { + if (!hasPermissionsToHandleWidgets(roomId)) { + throw WidgetManagementFailure.NotEnoughPower + } + val params = CreateWidgetTask.Params( + roomId = roomId, + widgetId = widgetId, + content = emptyMap() + ) + createWidgetTask.execute(params) + } + } + + fun hasPermissionsToHandleWidgets(roomId: String): Boolean { + val powerLevelsEvent = stateEventDataSource.getStateEvent( + roomId = roomId, + eventType = EventType.STATE_ROOM_POWER_LEVELS, + stateKey = QueryStringValue.NoCondition + ) + val powerLevelsContent = powerLevelsEvent?.content?.toModel() ?: return false + return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, null) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetModule.kt new file mode 100644 index 0000000000..79c2ac0dbb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetModule.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets + +import dagger.Binds +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator +import im.vector.matrix.android.api.session.widgets.WidgetService +import im.vector.matrix.android.api.session.widgets.WidgetURLFormatter +import im.vector.matrix.android.internal.session.widgets.token.DefaultGetScalarTokenTask +import im.vector.matrix.android.internal.session.widgets.token.GetScalarTokenTask +import retrofit2.Retrofit + +@Module +internal abstract class WidgetModule { + + @Module + companion object { + @JvmStatic + @Provides + fun providesWidgetsAPI(retrofit: Retrofit): WidgetsAPI { + return retrofit.create(WidgetsAPI::class.java) + } + } + + @Binds + abstract fun bindWidgetService(service: DefaultWidgetService): WidgetService + + @Binds + abstract fun bindWidgetURLBuilder(formatter: DefaultWidgetURLFormatter): WidgetURLFormatter + + @Binds + abstract fun bindWidgetPostAPIMediator(mediator: DefaultWidgetPostAPIMediator): WidgetPostAPIMediator + + @Binds + abstract fun bindCreateWidgetTask(task: DefaultCreateWidgetTask): CreateWidgetTask + + @Binds + abstract fun bindGetScalarTokenTask(task: DefaultGetScalarTokenTask): GetScalarTokenTask +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetPostMessageAPIProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetPostMessageAPIProvider.kt new file mode 100644 index 0000000000..4f34960f4b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetPostMessageAPIProvider.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets + +import android.content.Context +import timber.log.Timber +import javax.inject.Inject + +internal class WidgetPostMessageAPIProvider @Inject constructor(private val context: Context) { + + private var postMessageAPIString: String? = null + + fun get(): String? { + if (postMessageAPIString == null) { + postMessageAPIString = readFromAsset(context) + } + return postMessageAPIString + } + + private fun readFromAsset(context: Context): String? { + return try { + context.assets.open("postMessageAPI.js").bufferedReader().use { + it.readText() + } + } catch (failure: Throwable) { + Timber.e(failure, "Reading postMessageAPI.js asset failed") + null + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetsAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetsAPI.kt new file mode 100644 index 0000000000..f798e7e25b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetsAPI.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.widgets + +import im.vector.matrix.android.internal.session.openid.RequestOpenIdTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +internal interface WidgetsAPI { + + /** + * register to the server + * + * @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961) + */ + @POST("register") + fun register(@Body body: RequestOpenIdTokenResponse, @Query("v") version: String?): Call + + @GET("account") + fun validateToken(@Query("scalar_token") scalarToken: String?, @Query("v") version: String?): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetsAPIProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetsAPIProvider.kt new file mode 100644 index 0000000000..05d10dee56 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetsAPIProvider.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets + +import dagger.Lazy +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.session.SessionScope +import okhttp3.OkHttpClient +import javax.inject.Inject + +@SessionScope +internal class WidgetsAPIProvider @Inject constructor(@Unauthenticated private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory) { + + // Map to keep one WidgetAPI instance by serverUrl + private val widgetsAPIs = mutableMapOf() + + fun get(serverUrl: String): WidgetsAPI { + return widgetsAPIs.getOrPut(serverUrl) { + retrofitFactory.create(okHttpClient, serverUrl).create(WidgetsAPI::class.java) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/UserAccountWidgets.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/UserAccountWidgets.kt new file mode 100644 index 0000000000..113047246e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/UserAccountWidgets.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets.helper + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import im.vector.matrix.android.api.session.widgets.model.Widget + +internal fun UserAccountDataEvent.extractWidgetSequence(widgetFactory: WidgetFactory): Sequence { + return content.asSequence() + .mapNotNull { + @Suppress("UNCHECKED_CAST") + (it.value as? JsonDict)?.toModel() + }.mapNotNull { event -> + widgetFactory.create(event) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/WidgetFactory.kt new file mode 100644 index 0000000000..4091b6defd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/WidgetFactory.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets.helper + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.sender.SenderInfo +import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.matrix.android.api.session.widgets.model.WidgetContent +import im.vector.matrix.android.api.session.widgets.model.WidgetType +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper +import im.vector.matrix.android.internal.session.user.UserDataSource +import io.realm.Realm +import io.realm.RealmConfiguration +import java.net.URLEncoder +import javax.inject.Inject + +internal class WidgetFactory @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, + private val userDataSource: UserDataSource, + @UserId private val userId: String) { + + fun create(widgetEvent: Event): Widget? { + val widgetContent = widgetEvent.content.toModel() + if (widgetContent?.url == null) return null + val widgetId = widgetEvent.stateKey ?: return null + val type = widgetContent.type ?: return null + val senderInfo = if (widgetEvent.senderId == null || widgetEvent.roomId == null) { + null + } else { + Realm.getInstance(realmConfiguration).use { + val roomMemberHelper = RoomMemberHelper(it, widgetEvent.roomId) + val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(widgetEvent.senderId) + SenderInfo( + userId = widgetEvent.senderId, + displayName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + avatarUrl = roomMemberSummaryEntity?.avatarUrl + ) + } + } + val isAddedByMe = widgetEvent.senderId == userId + val computedUrl = widgetContent.computeURL(widgetEvent.roomId) + return Widget( + widgetContent = widgetContent, + event = widgetEvent, + widgetId = widgetId, + senderInfo = senderInfo, + isAddedByMe = isAddedByMe, + computedUrl = computedUrl, + type = WidgetType.fromString(type) + ) + } + + private fun WidgetContent.computeURL(roomId: String?): String? { + var computedUrl = url ?: return null + val myUser = userDataSource.getUser(userId) + computedUrl = computedUrl + .replace("\$matrix_user_id", userId) + .replace("\$matrix_display_name", myUser?.displayName ?: userId) + .replace("\$matrix_avatar_url", myUser?.avatarUrl ?: "") + + if (roomId != null) { + computedUrl = computedUrl.replace("\$matrix_room_id", roomId) + } + for ((key, value) in data) { + if (value is String) { + computedUrl = computedUrl.replace("$$key", URLEncoder.encode(value, "utf-8")) + } + } + return computedUrl + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/token/GetScalarTokenTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/token/GetScalarTokenTask.kt new file mode 100644 index 0000000000..f6d1ecb23b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/token/GetScalarTokenTask.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets.token + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask +import im.vector.matrix.android.internal.session.widgets.RegisterWidgetResponse +import im.vector.matrix.android.api.session.widgets.WidgetManagementFailure +import im.vector.matrix.android.internal.session.widgets.WidgetsAPI +import im.vector.matrix.android.internal.session.widgets.WidgetsAPIProvider +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface GetScalarTokenTask : Task { + + data class Params( + val serverUrl: String, + val forceRefresh: Boolean = false + ) +} + +private const val WIDGET_API_VERSION = "1.1" + +internal class DefaultGetScalarTokenTask @Inject constructor(private val widgetsAPIProvider: WidgetsAPIProvider, + private val scalarTokenStore: ScalarTokenStore, + private val getOpenIdTokenTask: GetOpenIdTokenTask) : GetScalarTokenTask { + + override suspend fun execute(params: GetScalarTokenTask.Params): String { + val widgetsAPI = widgetsAPIProvider.get(params.serverUrl) + return if (params.forceRefresh) { + scalarTokenStore.clearToken(params.serverUrl) + getNewScalarToken(widgetsAPI, params.serverUrl) + } else { + val scalarToken = scalarTokenStore.getToken(params.serverUrl) + if (scalarToken == null) { + getNewScalarToken(widgetsAPI, params.serverUrl) + } else { + validateToken(widgetsAPI, params.serverUrl, scalarToken) + } + } + } + + private suspend fun getNewScalarToken(widgetsAPI: WidgetsAPI, serverUrl: String): String { + val openId = getOpenIdTokenTask.execute(Unit) + val registerWidgetResponse = executeRequest(null) { + apiCall = widgetsAPI.register(openId, WIDGET_API_VERSION) + } + if (registerWidgetResponse.scalarToken == null) { + // Should not happen + throw IllegalStateException("Scalar token is null") + } + scalarTokenStore.setToken(serverUrl, registerWidgetResponse.scalarToken) + widgetsAPI.validateToken(registerWidgetResponse.scalarToken, WIDGET_API_VERSION) + return registerWidgetResponse.scalarToken + } + + private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String { + return try { + executeRequest(null) { + apiCall = widgetsAPI.validateToken(scalarToken, WIDGET_API_VERSION) + } + scalarToken + } catch (failure: Throwable) { + if (failure is Failure.ServerError && failure.httpCode == HttpsURLConnection.HTTP_FORBIDDEN) { + if (failure.error.code == MatrixError.M_TERMS_NOT_SIGNED) { + throw WidgetManagementFailure.TermsNotSignedException(serverUrl, scalarToken) + } else { + scalarTokenStore.clearToken(serverUrl) + getNewScalarToken(widgetsAPI, serverUrl) + } + } else { + throw failure + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/token/ScalarTokenStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/token/ScalarTokenStore.kt new file mode 100644 index 0000000000..c87392c2b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/token/ScalarTokenStore.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.widgets.token + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.ScalarTokenEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.util.awaitTransaction +import im.vector.matrix.android.internal.util.fetchCopyMap +import javax.inject.Inject + +internal class ScalarTokenStore @Inject constructor(private val monarchy: Monarchy) { + + fun getToken(apiUrl: String): String? { + return monarchy.fetchCopyMap({ realm -> + ScalarTokenEntity.where(realm, apiUrl).findFirst() + }, { scalarToken, _ -> + scalarToken.token + }) + } + + suspend fun setToken(apiUrl: String, token: String) { + monarchy.awaitTransaction { realm -> + val scalarTokenEntity = ScalarTokenEntity(apiUrl, token) + realm.insertOrUpdate(scalarTokenEntity) + } + } + + suspend fun clearToken(apiUrl: String) { + monarchy.awaitTransaction { realm -> + ScalarTokenEntity.where(realm, apiUrl).findFirst()?.deleteFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineToCallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineToCallback.kt index 54c19bd86f..07ab21a743 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineToCallback.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/CoroutineToCallback.kt @@ -22,7 +22,9 @@ import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.util.toCancelable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -34,5 +36,7 @@ internal fun CoroutineScope.launchToCallback( val result = runCatching { block() } - result.foldToCallback(callback) + withContext(Dispatchers.Main) { + result.foldToCallback(callback) + } }.toCancelable() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt index 6cba29ceec..61bb575c7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.util import android.content.res.Resources +import androidx.annotation.ArrayRes import androidx.annotation.NonNull import androidx.annotation.StringRes import dagger.Reusable @@ -53,4 +54,9 @@ internal class StringProvider @Inject constructor(private val resources: Resourc fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { return resources.getString(resId, *formatArgs) } + + @Throws(Resources.NotFoundException::class) + fun getStringArray(@ArrayRes id: Int): Array { + return resources.getStringArray(id) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt index 7c81a03223..bb5799baf1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt @@ -37,3 +37,14 @@ internal fun String.ensureProtocol(): String { else -> this } } + +/** + * Ensure string has trailing / + */ +internal fun String.ensureTrailingSlash(): String { + return when { + isEmpty() -> this + !endsWith("/") -> "$this/" + else -> this + } +} diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 69907e5835..3cd7674253 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -2,53 +2,105 @@ %1$s: %2$s %1$s sent an image. + You sent an image. %1$s sent a sticker. + You sent a sticker. %s\'s invitation + Your invitation %1$s created the room + You created the room %1$s invited %2$s + You invited %1$s %1$s invited you %1$s joined the room + You joined the room %1$s left the room + You left the room %1$s rejected the invitation + You rejected the invitation %1$s kicked %2$s + You kicked %1$s %1$s unbanned %2$s + You unbanned %1$s %1$s banned %2$s + You banned %1$s %1$s withdrew %2$s\'s invitation + You withdrew %1$s\'s invitation %1$s changed their avatar + You changed your avatar %1$s set their display name to %2$s + You set your display name to %1$s %1$s changed their display name from %2$s to %3$s + You changed your display name from %1$s to %2$s %1$s removed their display name (%2$s) + You removed your display name (%1$s) %1$s changed the topic to: %2$s + You changed the topic to: %1$s %1$s changed the room name to: %2$s + You changed the room name to: %1$s %s placed a video call. + You placed a video call. %s placed a voice call. + You placed a voice call. %s answered the call. + You answered the call. %s ended the call. + You ended the call. %1$s made future room history visible to %2$s + You made future room history visible to %1$s all room members, from the point they are invited. all room members, from the point they joined. all room members. anyone. unknown (%s). %1$s turned on end-to-end encryption (%2$s) + You turned on end-to-end encryption (%1$s) %s upgraded this room. + You upgraded this room. %1$s requested a VoIP conference + You requested a VoIP conference VoIP conference started VoIP conference finished (avatar was changed too) %1$s removed the room name + You removed the room name %1$s removed the room topic + You removed the room topic Message removed Message removed by %1$s Message removed [reason: %1$s] Message removed by %1$s [reason: %2$s] %1$s updated their profile %2$s + You updated your profile %1$s %1$s sent an invitation to %2$s to join the room + You sent an invitation to %1$s to join the room %1$s revoked the invitation for %2$s to join the room + You revoked the invitation for %1$s to join the room %1$s accepted the invitation for %2$s + You accepted the invitation for %1$s + + %1$s added %2$s widget + You added %1$s widget + %1$s removed %2$s widget + You removed %1$s widget + %1$s modified %2$s widget + You modified %1$s widget + + Admin + Moderator + Default + Custom (%1$d) + Custom + + + You changed the power level of %1$s. + + %1$s changed the power level of %2$s. + + %1$s from %2$s to %3$s ** Unable to decrypt: %s ** The sender\'s device has not sent us the keys for this message. @@ -245,39 +297,68 @@ Clear sending queue %1$s\'s invitation. Reason: %2$s + Your invitation. Reason: %1$s %1$s invited %2$s. Reason: %3$s + You invited %1$s. Reason: %2$s %1$s invited you. Reason: %2$s %1$s joined the room. Reason: %2$s + You joined the room. Reason: %1$s %1$s left the room. Reason: %2$s + You left the room. Reason: %1$s %1$s rejected the invitation. Reason: %2$s + You rejected the invitation. Reason: %1$s %1$s kicked %2$s. Reason: %3$s + You kicked %1$s. Reason: %2$s %1$s unbanned %2$s. Reason: %3$s + You unbanned %1$s. Reason: %2$s %1$s banned %2$s. Reason: %3$s + You banned %1$s. Reason: %2$s %1$s sent an invitation to %2$s to join the room. Reason: %3$s + You sent an invitation to %1$s to join the room. Reason: %2$s %1$s revoked the invitation for %2$s to join the room. Reason: %3$s + You revoked the invitation for %1$s to join the room. Reason: %2$s %1$s accepted the invitation for %2$s. Reason: %3$s + You accepted the invitation for %1$s. Reason: %2$s %1$s withdrew %2$s\'s invitation. Reason: %3$s + You withdrew %1$s\'s invitation. Reason: %2$s %1$s added %2$s as an address for this room. %1$s added %2$s as addresses for this room. + + You added %1$s as an address for this room. + You added %1$s as addresses for this room. + + %1$s removed %2$s as an address for this room. %1$s removed %3$s as addresses for this room. + + You removed %1$s as an address for this room. + You removed %2$s as addresses for this room. + + %1$s added %2$s and removed %3$s as addresses for this room. + You added %1$s and removed %2$s as addresses for this room. "%1$s set the main address for this room to %2$s." + "You set the main address for this room to %1$s." "%1$s removed the main address for this room." + "You removed the main address for this room." "%1$s has allowed guests to join the room." + "You have allowed guests to join the room." "%1$s has prevented guests from joining the room." + "You have prevented guests from joining the room." %1$s turned on end-to-end encryption. + You turned on end-to-end encryption. %1$s turned on end-to-end encryption (unrecognised algorithm %2$s). + You turned on end-to-end encryption (unrecognised algorithm %1$s). %s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys. diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/auth/data/VersionsKtTest.kt new file mode 100644 index 0000000000..fa6e47f96d --- /dev/null +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/auth/data/VersionsKtTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.data + +import im.vector.matrix.android.internal.auth.version.Versions +import im.vector.matrix.android.internal.auth.version.isSupportedBySdk +import org.amshove.kluent.shouldBe +import org.junit.Test + +class VersionsKtTest { + + @Test + fun isSupportedBySdkTooLow() { + Versions(supportedVersions = listOf("r0.4.0")).isSupportedBySdk() shouldBe false + Versions(supportedVersions = listOf("r0.4.1")).isSupportedBySdk() shouldBe false + } + + @Test + fun isSupportedBySdkUnstable() { + Versions(supportedVersions = listOf("r0.4.0"), unstableFeatures = mapOf("m.lazy_load_members" to true)).isSupportedBySdk() shouldBe true + } + + @Test + fun isSupportedBySdkOk() { + Versions(supportedVersions = listOf("r0.5.0")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.5.1")).isSupportedBySdk() shouldBe true + } + + // Was not working + @Test + fun isSupportedBySdkLater() { + Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.6.1")).isSupportedBySdk() shouldBe true + } + + // Cover cases of issue #1442 + @Test + fun isSupportedBySdk1442() { + Versions(supportedVersions = listOf("r0.5.0", "r0.6.0")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.5.0", "r0.6.1")).isSupportedBySdk() shouldBe true + Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true + } +} diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 3c0337ac99..d46bdfa062 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -120,7 +120,8 @@ ButterKnife\.findById\( \w\.flatMap\( ### Bad formatting of Realm query chain. Insert new line -\)\.equalTo +# DISABLED +# \)\.equalTo # Use StandardCharsets.UTF_8.name() # DISABLED (min API to low) diff --git a/vector/build.gradle b/vector/build.gradle index 46dcd92ab1..913b25c23a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 21 +ext.versionMinor = 22 ext.versionPatch = 0 ext.scVersion = 5 @@ -249,7 +249,7 @@ android { dependencies { - def epoxy_version = '3.9.0' + def epoxy_version = '3.11.0' def fragment_version = '1.2.0' def arrow_version = "0.8.2" def coroutines_version = "1.3.2" @@ -333,6 +333,9 @@ dependencies { implementation 'com.google.android:flexbox:1.1.1' implementation "androidx.autofill:autofill:$autofill_version" + // Custom Tab + implementation 'androidx.browser:browser:1.2.0' + // Passphrase strength helper implementation 'com.nulab-inc:zxcvbn:1.2.7' @@ -394,6 +397,9 @@ dependencies { // Plant Timber tree for test testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' + // Activate when you want to check for leaks, from time to time. + //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3' + androidTestImplementation 'androidx.test:core:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 83cba5292d..2911794baa 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -48,7 +48,19 @@ + android:launchMode="singleTask" + android:windowSoftInputMode="adjustResize"> + + + + + + + + + + + + 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 index 7e0407fef4..0b7731ee70 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -59,6 +59,7 @@ import im.vector.riotx.features.login.LoginResetPasswordSuccessFragment import im.vector.riotx.features.login.LoginServerSelectionFragment import im.vector.riotx.features.login.LoginServerUrlFormFragment import im.vector.riotx.features.login.LoginSignUpSignInSelectionFragment +import im.vector.riotx.features.login.LoginSignUpSignInSsoFragment import im.vector.riotx.features.login.LoginSplashFragment import im.vector.riotx.features.login.LoginWaitForEmailFragment import im.vector.riotx.features.login.LoginWebFragment @@ -102,6 +103,7 @@ import im.vector.riotx.features.signout.soft.SoftLogoutFragment import im.vector.riotx.features.terms.ReviewTermsFragment import im.vector.riotx.features.userdirectory.KnownUsersFragment import im.vector.riotx.features.userdirectory.UserDirectoryFragment +import im.vector.riotx.features.widgets.WidgetFragment @Module interface FragmentModule { @@ -216,6 +218,11 @@ interface FragmentModule { @FragmentKey(LoginSignUpSignInSelectionFragment::class) fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LoginSignUpSignInSsoFragment::class) + fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment + @Binds @IntoMap @FragmentKey(LoginSplashFragment::class) @@ -510,4 +517,9 @@ interface FragmentModule { @IntoMap @FragmentKey(ReviewTermsFragment::class) fun bindReviewTermsFragment(fragment: ReviewTermsFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(WidgetFragment::class) + fun bindWidgetFragment(fragment: WidgetFragment): 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 7d61c13b83..f8ed0b01c4 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 @@ -32,10 +32,12 @@ import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.debug.DebugMenuActivity import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeModule +import im.vector.riotx.features.home.room.detail.RoomDetailActivity 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.detail.widget.RoomWidgetsBottomSheet import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListModule import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet @@ -56,13 +58,17 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.reactions.widget.ReactionButton import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity +import im.vector.riotx.features.roommemberprofile.RoomMemberProfileActivity import im.vector.riotx.features.roommemberprofile.devices.DeviceListBottomSheet +import im.vector.riotx.features.roomprofile.RoomProfileActivity import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.devices.DeviceVerificationInfoBottomSheet import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.signout.soft.SoftLogoutActivity import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.ui.UiStateRepository +import im.vector.riotx.features.widgets.WidgetActivity +import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet @Component( dependencies = [ @@ -98,6 +104,9 @@ interface ScreenComponent { * ========================================================================================== */ fun inject(activity: HomeActivity) + fun inject(activity: RoomDetailActivity) + fun inject(activity: RoomProfileActivity) + fun inject(activity: RoomMemberProfileActivity) fun inject(activity: VectorSettingsActivity) fun inject(activity: KeysBackupManageActivity) fun inject(activity: EmojiReactionPickerActivity) @@ -120,6 +129,7 @@ interface ScreenComponent { fun inject(activity: BigImageViewerActivity) fun inject(activity: InviteUsersToRoomActivity) fun inject(activity: ReviewTermsActivity) + fun inject(activity: WidgetActivity) /* ========================================================================================== * BottomSheets @@ -134,6 +144,8 @@ interface ScreenComponent { fun inject(bottomSheet: DeviceVerificationInfoBottomSheet) fun inject(bottomSheet: DeviceListBottomSheet) fun inject(bottomSheet: BootstrapBottomSheet) + fun inject(bottomSheet: RoomWidgetPermissionBottomSheet) + fun inject(bottomSheet: RoomWidgetsBottomSheet) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/ConfirmationDialogBuilder.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/ConfirmationDialogBuilder.kt new file mode 100644 index 0000000000..aa95b29c4d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/ConfirmationDialogBuilder.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.dialogs + +import android.app.Activity +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import im.vector.riotx.R +import kotlinx.android.synthetic.main.dialog_confirmation_with_reason.view.* + +object ConfirmationDialogBuilder { + + fun show(activity: Activity, + askForReason: Boolean, + @StringRes titleRes: Int, + @StringRes confirmationRes: Int, + @StringRes positiveRes: Int, + @StringRes reasonHintRes: Int, + confirmation: (String?) -> Unit) { + val layout = activity.layoutInflater.inflate(R.layout.dialog_confirmation_with_reason, null) + layout.dialogConfirmationText.setText(confirmationRes) + + layout.dialogReasonCheck.isVisible = askForReason + layout.dialogReasonTextInputLayout.isVisible = askForReason + + layout.dialogReasonCheck.setOnCheckedChangeListener { _, isChecked -> + layout.dialogReasonTextInputLayout.isEnabled = isChecked + } + if (askForReason && reasonHintRes != 0) { + layout.dialogReasonInput.setHint(reasonHintRes) + } + + AlertDialog.Builder(activity) + .setTitle(titleRes) + .setView(layout) + .setPositiveButton(positiveRes) { _, _ -> + val reason = layout.dialogReasonInput.text.toString() + .takeIf { askForReason } + ?.takeIf { layout.dialogReasonCheck.isChecked } + ?.takeIf { it.isNotBlank() } + confirmation(reason) + } + .setNegativeButton(R.string.cancel, null) + .show() + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/LayoutManagerStateRestorer.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/LayoutManagerStateRestorer.kt index 3594bd23ff..1ee3d5e2b6 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/LayoutManagerStateRestorer.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/LayoutManagerStateRestorer.kt @@ -25,12 +25,21 @@ import java.util.concurrent.atomic.AtomicReference private const val LAYOUT_MANAGER_STATE = "LAYOUT_MANAGER_STATE" -class LayoutManagerStateRestorer(private val layoutManager: RecyclerView.LayoutManager) : Restorable, DefaultListUpdateCallback { +class LayoutManagerStateRestorer(layoutManager: RecyclerView.LayoutManager) : Restorable, DefaultListUpdateCallback { + private var layoutManager: RecyclerView.LayoutManager? = null private var layoutManagerState = AtomicReference() + init { + this.layoutManager = layoutManager + } + + fun clear() { + layoutManager = null + } + override fun onSaveInstanceState(outState: Bundle) { - val layoutManagerState = layoutManager.onSaveInstanceState() + val layoutManagerState = layoutManager?.onSaveInstanceState() outState.putParcelable(LAYOUT_MANAGER_STATE, layoutManagerState) } @@ -41,7 +50,7 @@ class LayoutManagerStateRestorer(private val layoutManager: RecyclerView.LayoutM override fun onInserted(position: Int, count: Int) { layoutManagerState.getAndSet(null)?.also { - layoutManager.onRestoreInstanceState(it) + layoutManager?.onRestoreInstanceState(it) } } } diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt index b9c0fbdc35..1c1d613f3e 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt @@ -16,9 +16,12 @@ */ 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.core.content.ContextCompat +import androidx.core.widget.ImageViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.matrix.android.api.util.MatrixItem @@ -28,7 +31,9 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.onClick import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.themes.ThemeUtils /** * A room preview for bottom sheet. @@ -36,22 +41,46 @@ import im.vector.riotx.features.home.AvatarRenderer @EpoxyModelClass(layout = R.layout.item_bottom_sheet_room_preview) abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel() { - @EpoxyAttribute - lateinit var avatarRenderer: AvatarRenderer - @EpoxyAttribute - lateinit var matrixItem: MatrixItem + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute lateinit var stringProvider: StringProvider + @EpoxyAttribute var izFavorite: Boolean = false @EpoxyAttribute var settingsClickListener: ClickListener? = null + @EpoxyAttribute var favoriteClickListener: ClickListener? = null override fun bind(holder: Holder) { avatarRenderer.render(matrixItem, holder.avatar) holder.avatar.onClick(settingsClickListener) holder.roomName.setTextOrHide(matrixItem.displayName) + setFavoriteState(holder, izFavorite) + + holder.roomFavorite.setOnClickListener { + // Immediate echo + setFavoriteState(holder, !izFavorite) + // And do the action + favoriteClickListener?.invoke() + } holder.roomSettings.onClick(settingsClickListener) } + private fun setFavoriteState(holder: Holder, isFavorite: Boolean) { + val tintColor: Int + if (isFavorite) { + holder.roomFavorite.contentDescription = stringProvider.getString(R.string.room_list_quick_actions_favorite_remove) + holder.roomFavorite.setImageResource(R.drawable.ic_star_green_24dp) + tintColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent) + } else { + holder.roomFavorite.contentDescription = stringProvider.getString(R.string.room_list_quick_actions_favorite_add) + holder.roomFavorite.setImageResource(R.drawable.ic_star_24dp) + tintColor = ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary) + } + ImageViewCompat.setImageTintList(holder.roomFavorite, ColorStateList.valueOf(tintColor)) + } + class Holder : VectorEpoxyHolder() { val avatar by bind(R.id.bottomSheetRoomPreviewAvatar) val roomName by bind(R.id.bottomSheetRoomPreviewName) + val roomFavorite by bind(R.id.bottomSheetRoomPreviewFavorite) val roomSettings by bind(R.id.bottomSheetRoomPreviewSettings) } } diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt index 3c31c09c52..a015358d8b 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt @@ -66,7 +66,6 @@ abstract class ProfileActionItem : VectorEpoxyModel() if (listener == null) { holder.view.isClickable = false } - holder.editable.isVisible = editable holder.title.text = title val tintColor = if (destructive) { ContextCompat.getColor(holder.view.context, R.color.riotx_notice) @@ -94,7 +93,7 @@ abstract class ProfileActionItem : VectorEpoxyModel() holder.secondaryAccessory.isVisible = false } - if (editableRes != 0) { + if (editableRes != 0 && editable) { val tintColorSecondary = if (destructive) { tintColor } else { 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 770a63a3fa..270e67cf34 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 @@ -95,7 +95,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { protected val viewModelProvider get() = ViewModelProvider(this, viewModelFactory) - // TODO Other Activity should use this also protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { viewEvents .observe() diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt index 1c1f3fae1a..653975aea3 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt @@ -18,23 +18,19 @@ package im.vector.riotx.core.ui.views import android.content.Context import android.graphics.Color -import android.text.SpannableString -import android.text.TextPaint import android.text.method.LinkMovementMethod -import android.text.style.ClickableSpan import android.util.AttributeSet import android.view.View -import android.widget.ImageView import android.widget.RelativeLayout -import android.widget.TextView import androidx.core.content.ContextCompat -import butterknife.BindView -import butterknife.ButterKnife +import androidx.core.text.italic import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.session.events.model.Event import im.vector.riotx.R import im.vector.riotx.core.error.ResourceLimitErrorFormatter +import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.themes.ThemeUtils +import kotlinx.android.synthetic.main.view_notification_area.view.* import me.gujun.android.span.span import me.saket.bettermovementmethod.BetterLinkMovementMethod import timber.log.Timber @@ -49,11 +45,6 @@ class NotificationAreaView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : RelativeLayout(context, attrs, defStyleAttr) { - @BindView(R.id.room_notification_icon) - lateinit var imageView: ImageView - @BindView(R.id.room_notification_message) - lateinit var messageView: TextView - var delegate: Delegate? = null private var state: State = State.Initial @@ -77,13 +68,9 @@ class NotificationAreaView @JvmOverloads constructor( when (newState) { is State.Default -> renderDefault() is State.Hidden -> renderHidden() + is State.NoPermissionToPost -> renderNoPermissionToPost() is State.Tombstone -> renderTombstone(newState) is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState) - is State.ConnectionError -> renderConnectionError() - is State.Typing -> renderTyping(newState) - is State.UnreadPreview -> renderUnreadPreview() - is State.ScrollToBottom -> renderScrollToBottom(newState) - is State.UnsentEvents -> renderUnsent(newState) } } @@ -91,30 +78,27 @@ class NotificationAreaView @JvmOverloads constructor( private fun setupView() { inflate(context, R.layout.view_notification_area, this) - ButterKnife.bind(this) + minimumHeight = DimensionConverter(resources).dpToPx(48) } private fun cleanUp() { - messageView.setOnClickListener(null) - imageView.setOnClickListener(null) + roomNotificationMessage.setOnClickListener(null) + roomNotificationIcon.setOnClickListener(null) setBackgroundColor(Color.TRANSPARENT) - messageView.text = null - imageView.setImageResource(0) + roomNotificationMessage.text = null + roomNotificationIcon.setImageResource(0) } - private fun renderTombstone(state: State.Tombstone) { + private fun renderNoPermissionToPost() { visibility = View.VISIBLE - imageView.setImageResource(R.drawable.error) + roomNotificationIcon.setImageDrawable(null) val message = span { - +resources.getString(R.string.room_tombstone_versioned_description) - +"\n" - span(resources.getString(R.string.room_tombstone_continuation_link)) { - textDecorationLine = "underline" - onClick = { delegate?.onTombstoneEventClicked(state.tombstoneEvent) } + italic { + +resources.getString(R.string.room_do_not_have_permission_to_post) } } - messageView.movementMethod = BetterLinkMovementMethod.getInstance() - messageView.text = message + roomNotificationMessage.text = message + roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.riotx_text_secondary)) } private fun renderResourceLimitExceededError(state: State.ResourceLimitExceededError) { @@ -130,73 +114,26 @@ class NotificationAreaView @JvmOverloads constructor( formatterMode = ResourceLimitErrorFormatter.Mode.Hard } val message = resourceLimitErrorFormatter.format(state.matrixError, formatterMode, clickable = true) - messageView.setTextColor(Color.WHITE) - messageView.text = message - messageView.movementMethod = LinkMovementMethod.getInstance() - messageView.setLinkTextColor(Color.WHITE) + roomNotificationMessage.setTextColor(Color.WHITE) + roomNotificationMessage.text = message + roomNotificationMessage.movementMethod = LinkMovementMethod.getInstance() + roomNotificationMessage.setLinkTextColor(Color.WHITE) setBackgroundColor(ContextCompat.getColor(context, backgroundColor)) } - private fun renderConnectionError() { + private fun renderTombstone(state: State.Tombstone) { visibility = View.VISIBLE - imageView.setImageResource(R.drawable.error) - messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color)) - messageView.text = SpannableString(resources.getString(R.string.room_offline_notification)) - } - - private fun renderTyping(state: State.Typing) { - visibility = View.VISIBLE - imageView.setImageResource(R.drawable.vector_typing) - messageView.text = SpannableString(state.message) - messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) - } - - private fun renderUnreadPreview() { - visibility = View.VISIBLE - imageView.setImageResource(R.drawable.scrolldown) - messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) - imageView.setOnClickListener { delegate?.closeScreen() } - } - - private fun renderScrollToBottom(state: State.ScrollToBottom) { - visibility = View.VISIBLE - if (state.unreadCount > 0) { - imageView.setImageResource(R.drawable.newmessages) - messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color)) - messageView.text = SpannableString(resources.getQuantityString(R.plurals.room_new_messages_notification, state.unreadCount, state.unreadCount)) - } else { - imageView.setImageResource(R.drawable.scrolldown) - messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color)) - if (!state.message.isNullOrEmpty()) { - messageView.text = SpannableString(state.message) + roomNotificationIcon.setImageResource(R.drawable.error) + val message = span { + +resources.getString(R.string.room_tombstone_versioned_description) + +"\n" + span(resources.getString(R.string.room_tombstone_continuation_link)) { + textDecorationLine = "underline" + onClick = { delegate?.onTombstoneEventClicked(state.tombstoneEvent) } } } - messageView.setOnClickListener { delegate?.jumpToBottom() } - imageView.setOnClickListener { delegate?.jumpToBottom() } - } - - private fun renderUnsent(state: State.UnsentEvents) { - visibility = View.VISIBLE - imageView.setImageResource(R.drawable.error) - val cancelAll = resources.getString(R.string.room_prompt_cancel) - val resendAll = resources.getString(R.string.room_prompt_resend) - val messageRes = if (state.hasUnknownDeviceEvents) R.string.room_unknown_devices_messages_notification else R.string.room_unsent_messages_notification - val message = context.getString(messageRes, resendAll, cancelAll) - val cancelAllPos = message.indexOf(cancelAll) - val resendAllPos = message.indexOf(resendAll) - val spannableString = SpannableString(message) - // cancelAllPos should always be > 0 but a GA crash reported here - if (cancelAllPos >= 0) { - spannableString.setSpan(CancelAllClickableSpan(), cancelAllPos, cancelAllPos + cancelAll.length, 0) - } - - // resendAllPos should always be > 0 but a GA crash reported here - if (resendAllPos >= 0) { - spannableString.setSpan(ResendAllClickableSpan(), resendAllPos, resendAllPos + resendAll.length, 0) - } - messageView.movementMethod = LinkMovementMethod.getInstance() - messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color)) - messageView.text = spannableString + roomNotificationMessage.movementMethod = BetterLinkMovementMethod.getInstance() + roomNotificationMessage.text = message } private fun renderDefault() { @@ -207,44 +144,9 @@ class NotificationAreaView @JvmOverloads constructor( visibility = View.GONE } - /** - * Track the cancel all click. - */ - private inner class CancelAllClickableSpan : ClickableSpan() { - override fun onClick(widget: View) { - delegate?.deleteUnsentEvents() - render(state) - } - - override fun updateDrawState(ds: TextPaint) { - super.updateDrawState(ds) - ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color) - ds.bgColor = 0 - ds.isUnderlineText = true - } - } - - /** - * Track the resend all click. - */ - private inner class ResendAllClickableSpan : ClickableSpan() { - override fun onClick(widget: View) { - delegate?.resendUnsentEvents() - render(state) - } - - override fun updateDrawState(ds: TextPaint) { - super.updateDrawState(ds) - ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color) - ds.bgColor = 0 - ds.isUnderlineText = true - } - } - /** * The state representing the view * It can take one state at a time - * Priority of state is managed in {@link VectorRoomActivity.refreshNotificationsArea() } */ sealed class State { @@ -254,29 +156,17 @@ class NotificationAreaView @JvmOverloads constructor( // View will be Invisible object Default : State() + // User can't post messages to room because his power level doesn't allow it. + object NoPermissionToPost : State() + // View will be Gone object Hidden : State() - // Resource limit exceeded error will be displayed (only hard for the moment) - data class ResourceLimitExceededError(val isSoft: Boolean, val matrixError: MatrixError) : State() - - // Server connection is lost - object ConnectionError : State() - // The room is dead data class Tombstone(val tombstoneEvent: Event) : State() - // Somebody is typing - data class Typing(val message: String) : State() - - // Some new messages are unread in preview - object UnreadPreview : State() - - // Some new messages are unread (grey or red) - data class ScrollToBottom(val unreadCount: Int, val message: String? = null) : State() - - // Some event has been unsent - data class UnsentEvents(val hasUndeliverableEvents: Boolean, val hasUnknownDeviceEvents: Boolean) : State() + // Resource limit exceeded error will be displayed (only hard for the moment) + data class ResourceLimitExceededError(val isSoft: Boolean, val matrixError: MatrixError) : State() } /** @@ -284,31 +174,5 @@ class NotificationAreaView @JvmOverloads constructor( */ interface Delegate { fun onTombstoneEventClicked(tombstoneEvent: Event) - fun resendUnsentEvents() - fun deleteUnsentEvents() - fun closeScreen() - fun jumpToBottom() - } - - companion object { - /** - * Preference key. - */ - private const val SHOW_INFO_AREA_KEY = "SETTINGS_SHOW_INFO_AREA_KEY" - - /** - * Always show the info area. - */ - private const val SHOW_INFO_AREA_VALUE_ALWAYS = "always" - - /** - * Show the info area when it has messages or errors. - */ - private const val SHOW_INFO_AREA_VALUE_MESSAGES_AND_ERRORS = "messages_and_errors" - - /** - * Show the info area only when it has errors. - */ - private const val SHOW_INFO_AREA_VALUE_ONLY_ERRORS = "only_errors" } } 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 e46d756523..81be9620d0 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 @@ -21,10 +21,14 @@ import android.content.ActivityNotFoundException import android.content.ContentValues import android.content.Context import android.content.Intent +import android.graphics.BitmapFactory import android.net.Uri import android.os.Build import android.provider.Browser import android.provider.MediaStore +import androidx.browser.customtabs.CustomTabsIntent +import androidx.browser.customtabs.CustomTabsSession +import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.fragment.app.Fragment import im.vector.riotx.BuildConfig @@ -64,6 +68,34 @@ fun openUrlInExternalBrowser(context: Context, uri: Uri?) { } } +/** + * Open url in custom tab or, if not available, in the default browser + * If several compatible browsers are installed, the user will be proposed to choose one. + * Ref: https://developer.chrome.com/multidevice/android/customtabs + */ +fun openUrlInChromeCustomTab(context: Context, session: CustomTabsSession?, url: String) { + try { + CustomTabsIntent.Builder() + .setToolbarColor(ContextCompat.getColor(context, R.color.riotx_background_light)) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setNavigationBarColor(ContextCompat.getColor(context, R.color.riotx_header_panel_background_light)) + } + } + .setNavigationBarColor(ContextCompat.getColor(context, R.color.riotx_background_light)) + .setColorScheme(CustomTabsIntent.COLOR_SCHEME_LIGHT) + // Note: setting close button icon does not work + .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp)) + .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) + .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) + .apply { session?.let { setSession(it) } } + .build() + .launchUrl(context, Uri.parse(url)) + } catch (activityNotFoundException: ActivityNotFoundException) { + context.toast(R.string.error_no_external_application_found) + } +} + /** * Open sound recorder external application */ diff --git a/vector/src/main/java/im/vector/riotx/core/utils/JsonViewerStyler.kt b/vector/src/main/java/im/vector/riotx/core/utils/JsonViewerStyler.kt new file mode 100644 index 0000000000..94b08b22d6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/JsonViewerStyler.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.utils + +import im.vector.riotx.R +import im.vector.riotx.core.resources.ColorProvider +import org.billcarsonfr.jsonviewer.JSonViewerStyleProvider + +fun createJSonViewerStyleProvider(colorProvider: ColorProvider): JSonViewerStyleProvider { + return JSonViewerStyleProvider( + keyColor = colorProvider.getColor(R.color.riotx_accent), + secondaryColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary), + stringColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color), + baseColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary), + booleanColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color), + numberColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color) + ) +} diff --git a/vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt b/vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt index dbfc414793..a79b16ab1d 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt @@ -19,9 +19,7 @@ package im.vector.riotx.core.utils import android.content.Context import androidx.annotation.ColorRes import im.vector.riotx.R -import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.features.themes.ThemeUtils -import org.billcarsonfr.jsonviewer.JSonViewerStyleProvider import kotlin.math.abs @ColorRes @@ -43,14 +41,3 @@ fun getColorFromUserId(userId: String?, context: Context? = null): Int { else -> R.color.riotx_username_1 } } - -fun jsonViewerStyler(colorProvider: ColorProvider): JSonViewerStyleProvider { - return JSonViewerStyleProvider( - keyColor = colorProvider.getColor(R.color.riotx_accent), - secondaryColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary), - stringColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color), - baseColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary), - booleanColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color), - numberColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color) - ) -} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/RecyclerViewPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/RecyclerViewPresenter.kt new file mode 100644 index 0000000000..1e24a904d9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/RecyclerViewPresenter.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.autocomplete + +import android.content.Context +import android.database.DataSetObserver +import android.view.ViewGroup +import androidx.annotation.CallSuper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.otaliastudios.autocomplete.AutocompletePresenter + +abstract class RecyclerViewPresenter(context: Context?) : AutocompletePresenter(context) { + + private var recyclerView: RecyclerView? = null + private var clicks: ClickProvider? = null + private var observer: RecyclerView.AdapterDataObserver? = null + + override fun registerClickProvider(provider: ClickProvider) { + clicks = provider + } + + override fun registerDataSetObserver(observer: DataSetObserver) { + this.observer = Observer(observer) + } + + @CallSuper + override fun getView(): ViewGroup { + val adapter = instantiateAdapter() + observer?.also { + adapter.registerAdapterDataObserver(it) + } + return RecyclerView(context).apply { + this.adapter = adapter + this.layoutManager = instantiateLayoutManager() + this.itemAnimator = null + } + } + + override fun onViewShown() {} + @CallSuper + override fun onViewHidden() { + observer?.also { + recyclerView?.adapter?.unregisterAdapterDataObserver(it) + } + recyclerView = null + observer = null + } + + /** + * Dispatch click event to Autocomplete.Callback. + * Should be called when items are clicked. + * + * @param item the clicked item. + */ + protected fun dispatchClick(item: T) { + if (clicks != null) clicks?.click(item) + } + + /** + * Request that the popup should recompute its dimensions based on a recent change in + * the view being displayed. + * + * This is already managed internally for [RecyclerView] events. + * Only use it for changes in other views that you have added to the popup, + * and only if one of the dimensions for the popup is WRAP_CONTENT . + */ + protected fun dispatchLayoutChange() { + if (observer != null) observer!!.onChanged() + } + + /** + * Provide an adapter for the recycler. + * This should be a fresh instance every time this is called. + * + * @return a new adapter. + */ + protected abstract fun instantiateAdapter(): RecyclerView.Adapter<*> + + /** + * Provides a layout manager for the recycler. + * This should be a fresh instance every time this is called. + * Defaults to a vertical LinearLayoutManager, which is guaranteed to work well. + * + * @return a new layout manager. + */ + protected fun instantiateLayoutManager(): RecyclerView.LayoutManager { + return LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + } + + private class Observer internal constructor(private val root: DataSetObserver) : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + root.onChanged() + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { + root.onChanged() + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { + root.onChanged() + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + root.onChanged() + } + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + root.onChanged() + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt index 84ae8db217..caadb3cc91 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/command/AutocompleteCommandPresenter.kt @@ -18,8 +18,8 @@ package im.vector.riotx.features.autocomplete.command import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.otaliastudios.autocomplete.RecyclerViewPresenter import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.RecyclerViewPresenter import im.vector.riotx.features.command.Command import javax.inject.Inject @@ -32,8 +32,6 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context, } override fun instantiateAdapter(): RecyclerView.Adapter<*> { - // Also remove animation - recyclerView?.itemAnimator = null return controller.adapter } @@ -51,4 +49,8 @@ class AutocompleteCommandPresenter @Inject constructor(context: Context, } controller.setData(data) } + + fun clear() { + controller.listener = null + } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt index 731b48af86..5f7622a4f3 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt @@ -18,8 +18,8 @@ package im.vector.riotx.features.autocomplete.emoji import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.otaliastudios.autocomplete.RecyclerViewPresenter import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.RecyclerViewPresenter import im.vector.riotx.features.reactions.data.EmojiDataSource import javax.inject.Inject @@ -32,9 +32,11 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context, controller.listener = this } + fun clear() { + controller.listener = null + } + override fun instantiateAdapter(): RecyclerView.Adapter<*> { - // Also remove animation - recyclerView?.itemAnimator = null return controller.adapter } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt index b6f45b477c..3c63dc65d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt @@ -18,12 +18,12 @@ package im.vector.riotx.features.autocomplete.group import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.otaliastudios.autocomplete.RecyclerViewPresenter import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.group.groupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.RecyclerViewPresenter import javax.inject.Inject class AutocompleteGroupPresenter @Inject constructor(context: Context, @@ -35,9 +35,11 @@ class AutocompleteGroupPresenter @Inject constructor(context: Context, controller.listener = this } + fun clear() { + controller.listener = null + } + override fun instantiateAdapter(): RecyclerView.Adapter<*> { - // Also remove animation - recyclerView?.itemAnimator = null return controller.adapter } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt index 4be19f2e73..77333b3d48 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.autocomplete.member import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.otaliastudios.autocomplete.RecyclerViewPresenter import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.query.QueryStringValue @@ -27,6 +26,7 @@ import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.RecyclerViewPresenter class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, @Assisted val roomId: String, @@ -40,14 +40,16 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, controller.listener = this } + fun clear() { + controller.listener = null + } + @AssistedInject.Factory interface Factory { fun create(roomId: String): AutocompleteMemberPresenter } override fun instantiateAdapter(): RecyclerView.Adapter<*> { - // Also remove animation - recyclerView?.itemAnimator = null return controller.adapter } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt index 17787a22ef..c845decdf9 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt @@ -18,12 +18,12 @@ package im.vector.riotx.features.autocomplete.room import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.otaliastudios.autocomplete.RecyclerViewPresenter import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.riotx.features.autocomplete.AutocompleteClickListener +import im.vector.riotx.features.autocomplete.RecyclerViewPresenter import javax.inject.Inject class AutocompleteRoomPresenter @Inject constructor(context: Context, @@ -36,8 +36,6 @@ class AutocompleteRoomPresenter @Inject constructor(context: Context, } override fun instantiateAdapter(): RecyclerView.Adapter<*> { - // Also remove animation - recyclerView?.itemAnimator = null return controller.adapter } @@ -58,4 +56,8 @@ class AutocompleteRoomPresenter @Inject constructor(context: Context, .sortedBy { it.displayName } controller.setData(rooms.toList()) } + + fun clear() { + controller.listener = null + } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageActivity.kt index db37107dd6..27ab860994 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageActivity.kt @@ -31,7 +31,6 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.platform.SimpleFragmentActivity -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity.* import javax.inject.Inject @@ -59,17 +58,9 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() { super.onCreate(savedInstanceState) toolbar.visibility = View.GONE - viewModel.viewEvents - .observe() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - observeViewEvents(it) - } - .disposeOnDestroy() + viewModel.observeViewEvents { observeViewEvents(it) } - viewModel.subscribe(this) { - renderState(it) - } + viewModel.subscribe(this) { renderState(it) } } override fun onBackPressed() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt index 848166381e..054c7a354a 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt @@ -22,22 +22,18 @@ import android.os.Bundle import android.view.View import android.view.inputmethod.EditorInfo import com.airbnb.mvrx.activityViewModel -import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.extensions.tryThis import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.startImportTextFromFileIntent import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.* import java.util.concurrent.TimeUnit import javax.inject.Inject -class SharedSecuredStorageKeyFragment @Inject constructor( - private val colorProvider: ColorProvider -) : VectorBaseFragment() { +class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_ssss_access_from_key @@ -48,7 +44,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor( ssss_restore_with_key_text.text = getString(R.string.enter_secret_storage_input_key) ssss_key_enter_edittext.editorActionEvents() - .debounce(300, TimeUnit.MILLISECONDS) + .throttleFirst(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { if (it.actionId == EditorInfo.IME_ACTION_DONE) { @@ -102,9 +98,6 @@ class SharedSecuredStorageKeyFragment @Inject constructor( super.onActivityResult(requestCode, resultCode, data) } - override fun invalidate() = withState(sharedViewModel) { _ -> - } - companion object { private const val IMPORT_FILE_REQ = 0 } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt index f5eb450fe1..95d8579a9b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -58,7 +58,7 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor( .colorizeMatchingText(key, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) ssss_passphrase_enter_edittext.editorActionEvents() - .debounce(300, TimeUnit.MILLISECONDS) + .throttleFirst(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { if (it.actionId == EditorInfo.IME_ACTION_DONE) { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt index fcedd2926e..fd7d269e01 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt @@ -56,7 +56,7 @@ class BootstrapAccountPasswordFragment @Inject constructor( bootstrapAccountPasswordEditText.hint = getString(R.string.account_password) bootstrapAccountPasswordEditText.editorActionEvents() - .debounce(300, TimeUnit.MILLISECONDS) + .throttleFirst(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { if (it.actionId == EditorInfo.IME_ACTION_DONE) { @@ -98,8 +98,6 @@ class BootstrapAccountPasswordFragment @Inject constructor( } override fun invalidate() = withState(sharedViewModel) { state -> - super.invalidate() - if (state.step is BootstrapStep.AccountPassword) { val isPasswordVisible = state.step.isPasswordVisible bootstrapAccountPasswordEditText.showPassword(isPasswordVisible, updateCursor = false) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt index df4d741bf1..ebb6416317 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt @@ -63,7 +63,7 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( } ssss_passphrase_enter_edittext.editorActionEvents() - .debounce(300, TimeUnit.MILLISECONDS) + .throttleFirst(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { if (it.actionId == EditorInfo.IME_ACTION_DONE) { @@ -96,13 +96,15 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( return@withState } val passphrase = ssss_passphrase_enter_edittext.text?.toString() - if (passphrase.isNullOrBlank()) { - ssss_passphrase_enter_til.error = getString(R.string.passphrase_empty_error_message) - } else if (passphrase != state.passphrase) { - ssss_passphrase_enter_til.error = getString(R.string.passphrase_passphrase_does_not_match) - } else { - view?.hideKeyboard() - sharedViewModel.handle(BootstrapActions.DoInitialize(passphrase)) + when { + passphrase.isNullOrBlank() -> + ssss_passphrase_enter_til.error = getString(R.string.passphrase_empty_error_message) + passphrase != state.passphrase -> + ssss_passphrase_enter_til.error = getString(R.string.passphrase_passphrase_does_not_match) + else -> { + view?.hideKeyboard() + sharedViewModel.handle(BootstrapActions.DoInitialize(passphrase)) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt index b70902631a..3c22260f7f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.crypto.recover +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.toRegistrationFlowResponse @@ -28,7 +29,6 @@ import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo import im.vector.matrix.android.api.session.securestorage.SsssKeySpec -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt index d1eee9ff3f..982f72c14e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -53,11 +53,11 @@ class BootstrapEnterPassphraseFragment @Inject constructor( ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_enter_passphrase) withState(sharedViewModel) { - // set initial value (usefull when coming back) + // set initial value (useful when coming back) ssss_passphrase_enter_edittext.setText(it.passphrase ?: "") } ssss_passphrase_enter_edittext.editorActionEvents() - .debounce(300, TimeUnit.MILLISECONDS) + .throttleFirst(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { if (it.actionId == EditorInfo.IME_ACTION_DONE) { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt index caf43721a0..0b8e201edd 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -57,11 +57,11 @@ class BootstrapMigrateBackupFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) withState(sharedViewModel) { - // set initial value (usefull when coming back) + // set initial value (useful when coming back) bootstrapMigrateEditText.setText(it.passphrase ?: "") } bootstrapMigrateEditText.editorActionEvents() - .debounce(300, TimeUnit.MILLISECONDS) + .throttleFirst(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { if (it.actionId == EditorInfo.IME_ACTION_DONE) { diff --git a/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListFragment.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListFragment.kt index e884761cdf..66c4a2325b 100644 --- a/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListFragment.kt @@ -22,6 +22,7 @@ import android.view.View import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup @@ -50,7 +51,6 @@ class GroupListFragment @Inject constructor( groupController.callback = this stateView.contentView = groupListView groupListView.configureWith(groupController) - viewModel.subscribe { renderState(it) } viewModel.observeViewEvents { when (it) { is GroupListViewEvents.OpenGroupSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) @@ -64,7 +64,7 @@ class GroupListFragment @Inject constructor( super.onDestroyView() } - private fun renderState(state: GroupListViewState) { + override fun invalidate() = withState(viewModel) { state -> when (state.asyncGroups) { is Incomplete -> stateView.state = StateView.State.Loading is Success -> stateView.state = StateView.State.Content 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 b574b11fb0..8c7be56606 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 @@ -55,7 +55,7 @@ import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import timber.log.Timber import javax.inject.Inject -class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { +class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory { private lateinit var sharedActionViewModel: HomeSharedActionViewModel @@ -66,6 +66,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var shortcutsHandler: ShortcutsHandler + @Inject lateinit var unknownDeviceViewModelFactory: UnknownDeviceDetectorSharedViewModel.Factory private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { @@ -79,6 +80,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { injector.inject(this) } + override fun create(initialState: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel { + return unknownDeviceViewModelFactory.create(initialState) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) diff --git a/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt index 657942457e..c3f6e4b074 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt @@ -22,7 +22,6 @@ import android.os.Build import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat -import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.utils.DimensionConverter @@ -58,7 +57,7 @@ class ShortcutsHandler @Inject constructor( .observeOn(Schedulers.computation()) .subscribe { rooms -> val shortcuts = rooms - .filter { room -> room.tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } } + .filter { room -> room.isFavorite } .take(n = 4) // Android only allows us to create 4 shortcuts .map { room -> val intent = RoomDetailActivity.shortcutIntent(context, room.roomId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt index 1a0d9baf15..a61d726ac7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -16,12 +16,16 @@ package im.vector.riotx.features.home +import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.Session @@ -32,7 +36,6 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo import im.vector.matrix.rx.rx -import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModelAction @@ -53,16 +56,32 @@ data class DeviceDetectionInfo( val currentSessionTrust: Boolean ) -class UnknownDeviceDetectorSharedViewModel( - session: Session, - private val vectorPreferences: VectorPreferences, - initialState: UnknownDevicesState) +class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted initialState: UnknownDevicesState, + session: Session, + private val vectorPreferences: VectorPreferences) : VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { data class IgnoreDevice(val deviceIds: List) : Action() } + @AssistedInject.Factory + interface Factory { + fun create(initialState: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + private val ignoredDeviceList = ArrayList() init { @@ -84,7 +103,7 @@ class UnknownDeviceDetectorSharedViewModel( session.rx().liveMyDeviceInfo(), session.rx().liveCrossSigningPrivateKeys(), Function3 { cryptoList, infoList, pInfo -> -// Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}") + // Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}") // Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}") infoList .filter { info -> @@ -106,7 +125,7 @@ class UnknownDeviceDetectorSharedViewModel( ) .distinctUntilChanged() .execute { async -> -// Timber.v("## Detector trigger passed distinct") + // Timber.v("## Detector trigger passed distinct") copy( myMatrixItem = session.getUser(session.myUserId)?.toMatrixItem(), unknownSessions = async @@ -146,12 +165,4 @@ class UnknownDeviceDetectorSharedViewModel( vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList) super.onCleared() } - - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? { - val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() - return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state) - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt index 5407c73f35..6bae5b604f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.breadcrumbs import android.os.Bundle import android.view.View import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith @@ -42,8 +43,6 @@ class BreadcrumbsFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) setupRecyclerView() sharedActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java) - - breadcrumbsViewModel.subscribe { renderState(it) } } override fun onDestroyView() { @@ -57,8 +56,7 @@ class BreadcrumbsFragment @Inject constructor( breadcrumbsController.listener = this } - // TODO Use invalidate() ? - private fun renderState(state: BreadcrumbsViewState) { + override fun invalidate() = withState(breadcrumbsViewModel) { state -> breadcrumbsController.update(state) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt index f8a1e302af..2e847176ff 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt @@ -21,7 +21,10 @@ 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.query.QueryStringValue 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.roomSummaryQueryParams import im.vector.matrix.rx.rx import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.EmptyViewEvents @@ -58,7 +61,10 @@ class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: B private fun observeBreadcrumbs() { session.rx() - .liveBreadcrumbs() + .liveBreadcrumbs(roomSummaryQueryParams { + displayName = QueryStringValue.NoCondition + memberships = listOf(Membership.JOIN) + }) .observeOn(Schedulers.computation()) .execute { asyncBreadcrumbs -> copy(asyncBreadcrumbs = asyncBreadcrumbs) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 314baab011..edc77bc83e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -34,6 +34,7 @@ import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toRoomAliasMatrixItem import im.vector.riotx.R import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.core.glide.GlideRequests import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter @@ -56,12 +57,14 @@ class AutoCompleter @AssistedInject constructor( private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter ) { + private lateinit var autocompleteMemberPresenter: AutocompleteMemberPresenter + @AssistedInject.Factory interface Factory { fun create(roomId: String): AutoCompleter } - private lateinit var editText: EditText + private var editText: EditText? = null fun enterSpecialMode() { commandAutocompletePolicy.enabled = false @@ -71,12 +74,11 @@ class AutoCompleter @AssistedInject constructor( commandAutocompletePolicy.enabled = true } - private val glideRequests by lazy { - GlideApp.with(editText) - } + private lateinit var glideRequests: GlideRequests fun setup(editText: EditText) { this.editText = editText + glideRequests = GlideApp.with(editText) val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background)) setupCommands(backgroundDrawable, editText) setupMembers(backgroundDrawable, editText) @@ -85,6 +87,15 @@ class AutoCompleter @AssistedInject constructor( setupRooms(backgroundDrawable, editText) } + fun clear() { + this.editText = null + autocompleteEmojiPresenter.clear() + autocompleteGroupPresenter.clear() + autocompleteRoomPresenter.clear() + autocompleteCommandPresenter.clear() + autocompleteMemberPresenter.clear() + } + private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) { Autocomplete.on(editText) .with(commandAutocompletePolicy) @@ -107,7 +118,7 @@ class AutoCompleter @AssistedInject constructor( } private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) { - val autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId) + autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId) Autocomplete.on(editText) .with(CharPolicy('@', true)) .with(autocompleteMemberPresenter) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 3230686d58..fba4f9e79e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail 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.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent 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 @@ -26,6 +27,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction sealed class RoomDetailAction : VectorViewModelAction { data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction() data class SaveDraft(val draft: String) : RoomDetailAction() + data class SendSticker(val stickerContent: MessageStickerContent) : RoomDetailAction() data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction() data class SendMedia(val attachments: List, val compressBeforeSending: Boolean) : RoomDetailAction() data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction() @@ -72,4 +74,6 @@ sealed class RoomDetailAction : VectorViewModelAction { data class RequestVerification(val userId: String) : RoomDetailAction() data class ResumeVerification(val transactionId: String, val otherUserId: String?) : RoomDetailAction() data class ReRequestKeys(val eventId: String) : RoomDetailAction() + + object SelectStickerAttachment : 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 6507bf6030..cd36222d9a 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 @@ -19,23 +19,48 @@ package im.vector.riotx.features.home.room.detail import android.content.Context import android.content.Intent import android.os.Bundle +import android.widget.Toast import androidx.appcompat.widget.Toolbar import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout +import com.airbnb.mvrx.viewModel 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.replaceFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment +import im.vector.riotx.features.room.RequireActiveMembershipAction +import im.vector.riotx.features.room.RequireActiveMembershipViewEvents +import im.vector.riotx.features.room.RequireActiveMembershipViewModel +import im.vector.riotx.features.room.RequireActiveMembershipViewState import kotlinx.android.synthetic.main.activity_room_detail.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* +import javax.inject.Inject -class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { +class RoomDetailActivity : + VectorBaseActivity(), + ToolbarConfigurable, + RequireActiveMembershipViewModel.Factory { override fun getLayoutRes() = R.layout.activity_room_detail private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel + private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel() + + @Inject + lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory + + override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel { + // Due to shortcut, we cannot use MvRx args. Pass the first roomId here + return requireActiveMembershipViewModelFactory.create(initialState.copy(roomId = currentRoomId ?: "")) + } + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } // Simple filter private var currentRoomId: String? = null @@ -68,14 +93,27 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { } .disposeOnDestroy() + requireActiveMembershipViewModel.observeViewEvents { + when (it) { + is RequireActiveMembershipViewEvents.RoomLeft -> handleRoomLeft(it) + } + } drawerLayout.addDrawerListener(drawerListener) } + private fun handleRoomLeft(roomLeft: RequireActiveMembershipViewEvents.RoomLeft) { + if (roomLeft.leftMessage != null) { + Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show() + } + finish() + } + private fun switchToRoom(switchToRoom: RoomDetailSharedAction.SwitchToRoom) { drawerLayout.closeDrawer(GravityCompat.START) // Do not replace the Fragment if it's the same roomId if (currentRoomId != switchToRoom.roomId) { currentRoomId = switchToRoom.roomId + requireActiveMembershipViewModel.handle(RequireActiveMembershipAction.ChangeRoom(switchToRoom.roomId)) replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, RoomDetailArgs(switchToRoom.roomId)) } } @@ -94,7 +132,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { hideKeyboard() if (!drawerLayout.isDrawerOpen(GravityCompat.START) && newState == DrawerLayout.STATE_DRAGGING) { - // User is starting to open the drawer, scroll the list to op + // User is starting to open the drawer, scroll the list to top scrollBreadcrumbsToTop() } } @@ -125,6 +163,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { } } + // Shortcuts can't have intents with parcelables fun shortcutIntent(context: Context, roomId: String): Intent { return Intent(context, RoomDetailActivity::class.java).apply { action = ACTION_ROOM_DETAILS_FROM_SHORTCUT 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 cb90059f55..205162dc1e 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 @@ -26,6 +26,7 @@ import android.os.Bundle import android.os.Parcelable import android.text.Spannable import android.view.HapticFeedbackConstants +import android.view.LayoutInflater import android.view.Menu import android.view.MenuItem import android.view.View @@ -55,16 +56,15 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session 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.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent @@ -72,6 +72,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFormat import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent +import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent @@ -80,10 +81,12 @@ import im.vector.matrix.android.api.session.room.send.SendState 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.getLastMessageContent +import im.vector.matrix.android.api.session.widgets.model.WidgetType import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R +import im.vector.riotx.core.dialogs.ConfirmationDialogBuilder import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.extensions.cleanup @@ -110,10 +113,10 @@ import im.vector.riotx.core.utils.allGranted import im.vector.riotx.core.utils.checkPermissions import im.vector.riotx.core.utils.colorizeMatchingText import im.vector.riotx.core.utils.copyToClipboard +import im.vector.riotx.core.utils.createJSonViewerStyleProvider import im.vector.riotx.core.utils.createUIHandler import im.vector.riotx.core.utils.getColorFromUserId import im.vector.riotx.core.utils.isValidUrl -import im.vector.riotx.core.utils.jsonViewerStyler import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.core.utils.saveMedia import im.vector.riotx.core.utils.shareMedia @@ -131,6 +134,7 @@ import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.composer.TextComposerView import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet +import im.vector.riotx.features.home.room.detail.sticker.StickerPickerConstants import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedAction import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet @@ -143,6 +147,8 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformatio import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet +import im.vector.riotx.features.home.room.detail.widget.RoomWidgetsBannerView +import im.vector.riotx.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.invite.VectorInviteView @@ -155,6 +161,7 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.themes.ThemeUtils +import im.vector.riotx.features.widgets.WidgetActivity import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import kotlinx.android.parcel.Parcelize @@ -194,7 +201,8 @@ class RoomDetailFragment @Inject constructor( VectorInviteView.Callback, JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, - AttachmentsHelper.Callback { + AttachmentsHelper.Callback, + RoomWidgetsBannerView.Callback { companion object { @@ -259,10 +267,11 @@ class RoomDetailFragment @Inject constructor( setupNotificationView() setupJumpToReadMarkerView() setupJumpToBottomView() + setupWidgetsBannerView() + roomToolbarContentView.debouncedClicks { navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) } - roomDetailViewModel.subscribe { renderState(it) } sharedActionViewModel .observe() @@ -275,7 +284,10 @@ class RoomDetailFragment @Inject constructor( renderTombstoneEventHandling(it) } - roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode -> + roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::canSendMessage) { mode, canSend -> + if (!canSend) { + return@selectSubscribe + } when (mode) { is SendMode.REGULAR -> renderRegularMode(mode.text) is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) @@ -290,20 +302,49 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.observeViewEvents { when (it) { - is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) - is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) - is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) - is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) - is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG) - is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) - is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it) - is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) - is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) - is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) + is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) + is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) + is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) + is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) + is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG) + is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) + is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it) + is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) + is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) + is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) + RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager() + is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it) }.exhaustive } } + private fun setupWidgetsBannerView() { + roomWidgetsBannerView.callback = this + } + + private fun openStickerPicker(event: RoomDetailViewEvents.OpenStickerPicker) { + navigator.openStickerPicker(this, roomDetailArgs.roomId, event.widget) + } + + private fun displayPromptForIntegrationManager() { + // The Sticker picker widget is not installed yet. Propose the user to install it + val builder = AlertDialog.Builder(requireContext()) + val v: View = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_no_sticker_pack, null) + builder + .setView(v) + .setPositiveButton(R.string.yes) { _, _ -> + // Open integration manager, to the sticker installation page + navigator.openIntegrationManager( + context = requireContext(), + roomId = roomDetailArgs.roomId, + integId = null, + screen = WidgetType.StickerPicker.preferred + ) + } + .setNegativeButton(R.string.no, null) + .show() + } + private fun handleJoinedToAnotherRoom(action: RoomDetailViewEvents.JoinRoomCommandSuccess) { updateComposerText("") lockSendButton = false @@ -331,8 +372,10 @@ class RoomDetailFragment @Inject constructor( timelineEventController.callback = null timelineEventController.removeModelBuildListener(modelBuildListener) modelBuildListener = null + autoCompleter.clear() debouncer.cancelAll() recyclerView.cleanup() + super.onDestroyView() } @@ -403,22 +446,6 @@ class RoomDetailFragment @Inject constructor( override fun onTombstoneEventClicked(tombstoneEvent: Event) { roomDetailViewModel.handle(RoomDetailAction.HandleTombstoneEvent(tombstoneEvent)) } - - override fun resendUnsentEvents() { - vectorBaseActivity.notImplemented() - } - - override fun deleteUnsentEvents() { - vectorBaseActivity.notImplemented() - } - - override fun closeScreen() { - vectorBaseActivity.notImplemented() - } - - override fun jumpToBottom() { - vectorBaseActivity.notImplemented() - } } } @@ -429,18 +456,24 @@ class RoomDetailFragment @Inject constructor( } override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.clear_message_queue) { - // 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.handle(RoomDetailAction.ClearSendQueue) - return true + return when (item.itemId) { + R.id.clear_message_queue -> { + // 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.handle(RoomDetailAction.ClearSendQueue) + true + } + R.id.resend_all -> { + roomDetailViewModel.handle(RoomDetailAction.ResendAll) + true + } + R.id.open_matrix_apps -> { + navigator.openIntegrationManager(requireContext(), roomDetailArgs.roomId, null, null) + true + } + else -> super.onOptionsItemSelected(item) } - if (item.itemId == R.id.resend_all) { - roomDetailViewModel.handle(RoomDetailAction.ResendAll) - return true - } - return super.onOptionsItemSelected(item) } private fun renderRegularMode(text: String) { @@ -515,15 +548,19 @@ class RoomDetailFragment @Inject constructor( val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data) if (!hasBeenHandled && resultCode == RESULT_OK && data != null) { when (requestCode) { - AttachmentsPreviewActivity.REQUEST_CODE -> { + AttachmentsPreviewActivity.REQUEST_CODE -> { val sendData = AttachmentsPreviewActivity.getOutput(data) val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data) roomDetailViewModel.handle(RoomDetailAction.SendMedia(sendData, !keepOriginalSize)) } - REACTION_SELECT_REQUEST_CODE -> { + REACTION_SELECT_REQUEST_CODE -> { val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction)) } + StickerPickerConstants.STICKER_PICKER_REQUEST_CODE -> { + val content = WidgetActivity.getOutput(data).toModel() ?: return + roomDetailViewModel.handle(RoomDetailAction.SendSticker(content)) + } } } } @@ -562,6 +599,12 @@ class RoomDetailFragment @Inject constructor( } override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + val canSendMessage = withState(roomDetailViewModel) { + it.canSendMessage + } + if (!canSendMessage) { + return false + } return when (model) { is MessageFileItem, is MessageImageVideoItem, @@ -623,6 +666,8 @@ class RoomDetailFragment @Inject constructor( return } if (text.isNotBlank()) { + // We collapse ASAP, if not there will be a slight anoying delay + composerLayout.collapse(true) lockSendButton = true roomDetailViewModel.handle(RoomDetailAction.SendMessage(text, vectorPreferences.isMarkdownEnabled())) } @@ -672,48 +717,47 @@ class RoomDetailFragment @Inject constructor( inviteView.callback = this } - private fun renderState(state: RoomDetailViewState) { + override fun invalidate() = withState(roomDetailViewModel) { state -> renderRoomSummary(state) val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { + roomWidgetsBannerView.render(state.activeRoomWidgets()) scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline timelineEventController.update(state) inviteView.visibility = View.GONE val uid = session.myUserId val meMember = state.myRoomMember() avatarRenderer.render(MatrixItem.UserItem(uid, meMember?.displayName, meMember?.avatarUrl), composerLayout.composerAvatarImageView) + if (state.tombstoneEvent == null) { + if (state.canSendMessage) { + composerLayout.visibility = View.VISIBLE + composerLayout.setRoomEncrypted(summary.isEncrypted, summary.roomEncryptionTrustLevel) + notificationAreaView.render(NotificationAreaView.State.Hidden) + } else { + composerLayout.visibility = View.GONE + notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost) + } + } else { + composerLayout.visibility = View.GONE + notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) + } } else if (summary?.membership == Membership.INVITE && inviter != null) { inviteView.visibility = View.VISIBLE inviteView.render(inviter, VectorInviteView.Mode.LARGE) - // Intercept click event inviteView.setOnClickListener { } } else if (state.asyncInviter.complete) { vectorBaseActivity.finish() } - val isRoomEncrypted = summary?.isEncrypted ?: false - if (state.tombstoneEvent == null) { - composerLayout.visibility = View.VISIBLE - composerLayout.setRoomEncrypted(isRoomEncrypted, state.asyncRoomSummary.invoke()?.roomEncryptionTrustLevel) - notificationAreaView.render(NotificationAreaView.State.Hidden) - } else { - composerLayout.visibility = View.GONE - notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) - } } private fun renderRoomSummary(state: RoomDetailViewState) { state.asyncRoomSummary()?.let { roomSummary -> - if (roomSummary.membership.isLeft()) { - Timber.w("The room has been left") - activity?.finish() - } else { - roomToolbarTitleView.text = roomSummary.displayName - avatarRenderer.render(roomSummary.toMatrixItem(), roomToolbarAvatarImageView) + roomToolbarTitleView.text = roomSummary.displayName + avatarRenderer.render(roomSummary.toMatrixItem(), roomToolbarAvatarImageView) - renderSubTitle(state.typingMessage, roomSummary.topic) - } + renderSubTitle(state.typingMessage, roomSummary.topic) jumpToBottomView.count = roomSummary.notificationCount jumpToBottomView.drawBadge = roomSummary.hasUnreadMessages @@ -813,28 +857,17 @@ class RoomDetailFragment @Inject constructor( } private fun promptConfirmationToRedactEvent(action: EventSharedAction.Redact) { - val layout = requireActivity().layoutInflater.inflate(R.layout.dialog_delete_event, null) - val reasonCheckBox = layout.findViewById(R.id.deleteEventReasonCheck) - val reasonTextInputLayout = layout.findViewById(R.id.deleteEventReasonTextInputLayout) - val reasonInput = layout.findViewById(R.id.deleteEventReasonInput) - - reasonCheckBox.isVisible = action.askForReason - reasonTextInputLayout.isVisible = action.askForReason - - reasonCheckBox.setOnCheckedChangeListener { _, isChecked -> reasonTextInputLayout.isEnabled = isChecked } - - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.delete_event_dialog_title) - .setView(layout) - .setPositiveButton(R.string.remove) { _, _ -> - val reason = reasonInput.text.toString() - .takeIf { action.askForReason } - ?.takeIf { reasonCheckBox.isChecked } - ?.takeIf { it.isNotBlank() } + ConfirmationDialogBuilder + .show( + activity = requireActivity(), + askForReason = action.askForReason, + confirmationRes = R.string.delete_event_dialog_content, + positiveRes = R.string.remove, + reasonHintRes = R.string.delete_event_dialog_reason_hint, + titleRes = R.string.delete_event_dialog_title + ) { reason -> roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, reason)) } - .setNegativeButton(R.string.cancel, null) - .show() } private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) { @@ -1209,14 +1242,14 @@ class RoomDetailFragment @Inject constructor( JSonViewerDialog.newInstance( action.content, -1, - jsonViewerStyler(colorProvider) + createJSonViewerStyleProvider(colorProvider) ).show(childFragmentManager, "JSON_VIEWER") } is EventSharedAction.ViewDecryptedSource -> { JSonViewerDialog.newInstance( action.content, -1, - jsonViewerStyler(colorProvider) + createJSonViewerStyleProvider(colorProvider) ).show(childFragmentManager, "JSON_VIEWER") } is EventSharedAction.QuickReact -> { @@ -1330,7 +1363,9 @@ class RoomDetailFragment @Inject constructor( } private fun focusComposerAndShowKeyboard() { - composerLayout.composerEditText.showKeyboard(andRequestFocus = true) + if (composerLayout.isVisible) { + composerLayout.composerEditText.showKeyboard(andRequestFocus = true) + } } private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { @@ -1382,7 +1417,7 @@ class RoomDetailFragment @Inject constructor( AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(this) AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(this) AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(this) - AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers") + AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment) }.exhaustive } @@ -1415,4 +1450,9 @@ class RoomDetailFragment @Inject constructor( val formattedContact = contactAttachment.toHumanReadable() roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false)) } + + override fun onViewWidgetsClicked() { + RoomWidgetsBottomSheet.newInstance() + .show(childFragmentManager, "ROOM_WIDGETS_BOTTOM_SHEET") + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt index b24c2ea23e..73ce95eda2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewEvents.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail import androidx.annotation.StringRes +import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.riotx.core.platform.VectorViewEvents import im.vector.riotx.features.command.Command import java.io.File @@ -49,6 +50,10 @@ sealed class RoomDetailViewEvents : VectorViewEvents { abstract class SendMessageResult : RoomDetailViewEvents() + object DisplayPromptForIntegrationManager: RoomDetailViewEvents() + + data class OpenStickerPicker(val widget: Widget): RoomDetailViewEvents() + object MessageSent : SendMessageResult() data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() class SlashCommandError(val command: Command) : SendMessageResult() 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 df6f46b431..69a6429efb 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 @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail import android.net.Uri import androidx.annotation.IdRes +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success @@ -34,6 +35,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.isImageMessage import im.vector.matrix.android.api.session.events.model.isTextMessage +import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities @@ -46,6 +48,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.OptionItem import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.Timeline @@ -67,13 +70,16 @@ import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator +import im.vector.riotx.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.home.room.typing.TypingHelper +import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory import im.vector.riotx.features.settings.VectorPreferences import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.launch import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import timber.log.Timber @@ -82,14 +88,15 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean class RoomDetailViewModel @AssistedInject constructor( - @Assisted initialState: RoomDetailViewState, + @Assisted private val initialState: RoomDetailViewState, userPreferencesProvider: UserPreferencesProvider, private val vectorPreferences: VectorPreferences, private val stringProvider: StringProvider, private val typingHelper: TypingHelper, private val rainbowGenerator: RainbowGenerator, private val session: Session, - private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider + private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, + private val stickerPickerActionHandler: StickerPickerActionHandler ) : VectorViewModel(initialState), Timeline.Listener { private val room = session.getRoom(initialState.roomId)!! @@ -100,12 +107,14 @@ class RoomDetailViewModel @AssistedInject constructor( TimelineSettings(30, filterEdits = false, filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(), + filterUseless = false, filterTypes = false, buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } else { TimelineSettings(30, filterEdits = true, filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(), + filterUseless = true, filterTypes = true, allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) @@ -155,6 +164,8 @@ class RoomDetailViewModel @AssistedInject constructor( observeDrafts() observeUnreadState() observeMyRoomMember() + observeActiveRoomWidgets() + observePowerLevel() room.getRoomSummaryLive() room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback()) room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() @@ -162,6 +173,31 @@ class RoomDetailViewModel @AssistedInject constructor( session.onRoomDisplayed(initialState.roomId) } + private fun observePowerLevel() { + PowerLevelsObservableFactory(room).createObservable() + .subscribe { + val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) + setState { + copy(canSendMessage = canSendMessage) + } + } + .disposeOnClear() + } + + private fun observeActiveRoomWidgets() { + session.rx() + .liveRoomWidgets( + roomId = initialState.roomId, + widgetId = QueryStringValue.NoCondition + ) + .map { widgets -> + widgets.filter { it.isActive } + } + .execute { + copy(activeRoomWidgets = it) + } + } + private fun observeMyRoomMember() { val queryParams = roomMemberQueryParams { this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE) @@ -183,6 +219,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.SaveDraft -> handleSaveDraft(action) is RoomDetailAction.SendMessage -> handleSendMessage(action) is RoomDetailAction.SendMedia -> handleSendMedia(action) + is RoomDetailAction.SendSticker -> handleSendSticker(action) is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) @@ -214,6 +251,18 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.RequestVerification -> handleRequestVerification(action) is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) + is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() + } + } + + private fun handleSendSticker(action: RoomDetailAction.SendSticker) { + room.sendEvent(EventType.STICKER, action.stickerContent.toContent()) + } + + private fun handleSelectStickerAttachment() { + viewModelScope.launch { + val viewEvent = stickerPickerActionHandler.handle() + _viewEvents.post(viewEvent) } } @@ -322,6 +371,7 @@ class RoomDetailViewModel @AssistedInject constructor( timeline.pendingEventCount() > 0 && vectorPreferences.developerMode() R.id.resend_all -> timeline.failedToDeliverEventCount() > 0 R.id.clear_all -> timeline.failedToDeliverEventCount() > 0 + R.id.open_matrix_apps -> session.integrationManagerService().isIntegrationEnabled() else -> false } @@ -372,16 +422,16 @@ class RoomDetailViewModel @AssistedInject constructor( popDraft() } is ParsedCommand.UnbanUser -> { - // TODO - _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) + handleUnbanSlashCommand(slashCommandResult) + popDraft() } is ParsedCommand.BanUser -> { - // TODO - _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) + handleBanSlashCommand(slashCommandResult) + popDraft() } is ParsedCommand.KickUser -> { - // TODO - _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) + handleKickSlashCommand(slashCommandResult) + popDraft() } is ParsedCommand.JoinRoom -> { handleJoinToAnotherRoomSlashCommand(slashCommandResult) @@ -567,23 +617,38 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) - - room.updateTopic(changeTopic.topic, object : MatrixCallback { - override fun onSuccess(data: Unit) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk) - } - - override fun onFailure(failure: Throwable) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) - } - }) + launchSlashCommandFlow { + room.updateTopic(changeTopic.topic, it) + } } private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { - _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + launchSlashCommandFlow { + room.invite(invite.userId, invite.reason, it) + } + } - room.invite(invite.userId, invite.reason, object : MatrixCallback { + private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) { + launchSlashCommandFlow { + room.kick(kick.userId, kick.reason, it) + } + } + + private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { + launchSlashCommandFlow { + room.ban(ban.userId, ban.reason, it) + } + } + + private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { + launchSlashCommandFlow { + room.unban(unban.userId, unban.reason, it) + } + } + + private fun launchSlashCommandFlow(lambda: (MatrixCallback) -> Unit) { + _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + val matrixCallback = object : MatrixCallback { override fun onSuccess(data: Unit) { _viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk) } @@ -591,7 +656,8 @@ class RoomDetailViewModel @AssistedInject constructor( override fun onFailure(failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) } - }) + } + lambda.invoke(matrixCallback) } private fun handleSendReaction(action: RoomDetailAction.SendReaction) { @@ -1006,7 +1072,7 @@ class RoomDetailViewModel @AssistedInject constructor( setState { copy(asyncInviter = Success(it)) } } } - room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, "")?.also { + room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE)?.also { setState { copy(tombstoneEvent = it) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index aa92b9a61b..44e42e761c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.util.MatrixItem /** @@ -55,6 +56,7 @@ data class RoomDetailViewState( val myRoomMember: Async = Uninitialized, val asyncInviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, + val activeRoomWidgets: Async> = Uninitialized, val typingRoomMembers: List? = null, val typingMessage: String? = null, val sendMode: SendMode = SendMode.REGULAR(""), @@ -63,7 +65,8 @@ data class RoomDetailViewState( val syncState: SyncState = SyncState.Idle, val highlightedEventId: String? = null, val unreadState: UnreadState = UnreadState.Unknown, - val canShowJumpToReadMarker: Boolean = true + val canShowJumpToReadMarker: Boolean = true, + val canSendMessage: Boolean = true ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/sticker/StickerPickerActionHandler.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/sticker/StickerPickerActionHandler.kt new file mode 100644 index 0000000000..3b939892b5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/sticker/StickerPickerActionHandler.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.sticker + +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.widgets.model.WidgetType +import im.vector.riotx.features.home.room.detail.RoomDetailViewEvents +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class StickerPickerActionHandler @Inject constructor(private val session: Session) { + + suspend fun handle(): RoomDetailViewEvents = withContext(Dispatchers.Default) { + // Search for the sticker picker widget in the user account + val stickerWidget = session.widgetService().getUserWidgets(WidgetType.StickerPicker.values()).firstOrNull { it.isActive } + if (stickerWidget == null || stickerWidget.computedUrl.isNullOrBlank()) { + RoomDetailViewEvents.DisplayPromptForIntegrationManager + } else { + RoomDetailViewEvents.OpenStickerPicker( + widget = stickerWidget + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/sticker/StickerPickerConstants.kt similarity index 75% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/sticker/StickerPickerConstants.kt index ffdea37afe..8068eafc85 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/sticker/StickerPickerConstants.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.api.auth.data -data class WellKnownManagerConfig( - val apiUrl: String, - val uiUrl: String -) +package im.vector.riotx.features.home.room.detail.sticker + +object StickerPickerConstants { + const val STICKER_PICKER_REQUEST_CODE = 16000 +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionState.kt new file mode 100644 index 0000000000..c12306c2d0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionState.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.timeline.action + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotx.core.extensions.canReact +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Quick reactions state + */ +data class ToggleState( + val reaction: String, + val isSelected: Boolean +) + +data class ActionPermissions( + val canSendMessage: Boolean = false, + val canReact: Boolean = false, + val canRedact: Boolean = false +) + +data class MessageActionState( + val roomId: String, + val eventId: String, + val informationData: MessageInformationData, + val timelineEvent: Async = Uninitialized, + val messageBody: CharSequence = "", + // For quick reactions + val quickStates: Async> = Uninitialized, + // For actions + val actions: List = emptyList(), + val expendedReportContentMenu: Boolean = false, + val actionPermissions: ActionPermissions = ActionPermissions() + ) : MvRxState { + + constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) + + private val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault()) + + fun senderName(): String = informationData.memberName?.toString() ?: "" + + fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: "" + + fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact +} 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 38db5440d6..909169e7b0 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 @@ -15,11 +15,8 @@ */ package im.vector.riotx.features.home.room.detail.timeline.action -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.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -35,6 +32,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent @@ -47,46 +45,11 @@ import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.VectorHtmlCompressor import im.vector.riotx.features.reactions.data.EmojiDataSource import im.vector.riotx.features.settings.VectorPreferences -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -/** - * Quick reactions state - */ -data class ToggleState( - val reaction: String, - val isSelected: Boolean -) - -data class MessageActionState( - val roomId: String, - val eventId: String, - val informationData: MessageInformationData, - val timelineEvent: Async = Uninitialized, - val messageBody: CharSequence = "", - // For quick reactions - val quickStates: Async> = Uninitialized, - // For actions - val actions: List = emptyList(), - val expendedReportContentMenu: Boolean = false -) : MvRxState { - - constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) - - private val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault()) - - fun senderName(): String = informationData.memberName?.toString() ?: "" - - fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: "" - - fun canReact() = timelineEvent()?.canReact() == true -} /** * Information related to an event and used to display preview in contextual bottom sheet. @@ -121,6 +84,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted init { observeEvent() observeReactions() + observePowerLevel() observeTimelineEventState() } @@ -138,6 +102,23 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } + private fun observePowerLevel() { + if (room == null) { + return + } + PowerLevelsObservableFactory(room).createObservable() + .subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + val canReact = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.REACTION) + val canRedact = powerLevelsHelper.isUserAbleToRedact(session.myUserId) + val canSendMessage = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) + val permissions = ActionPermissions(canSendMessage = canSendMessage, canRedact = canRedact, canReact = canReact) + setState { + copy(actionPermissions = permissions) + } + }.disposeOnClear() + } + private fun observeEvent() { if (room == null) return room.rx() @@ -163,11 +144,12 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun observeTimelineEventState() { - asyncSubscribe(MessageActionState::timelineEvent) { timelineEvent -> + selectSubscribe(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions -> + val nonNullTimelineEvent = timelineEvent() ?: return@selectSubscribe setState { copy( - messageBody = computeMessageBody(timelineEvent), - actions = actionsForEvent(timelineEvent) + messageBody = computeMessageBody(nonNullTimelineEvent), + actions = actionsForEvent(nonNullTimelineEvent, permissions) ) } } @@ -235,14 +217,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } - private fun actionsForEvent(timelineEvent: TimelineEvent): List { + private fun actionsForEvent(timelineEvent: TimelineEvent, actionPermissions: ActionPermissions): List { val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() ?: timelineEvent.root.getClearContent().toModel() val msgType = messageContent?.msgType return arrayListOf().apply { if (timelineEvent.root.sendState.hasFailed()) { - if (canRetry(timelineEvent)) { + if (canRetry(timelineEvent, actionPermissions)) { add(EventSharedAction.Resend(eventId)) } add(EventSharedAction.Remove(eventId)) @@ -253,15 +235,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } else if (timelineEvent.root.sendState == SendState.SYNCED) { if (!timelineEvent.root.isRedacted()) { - if (canReply(timelineEvent, messageContent)) { + if (canReply(timelineEvent, messageContent, actionPermissions)) { add(EventSharedAction.Reply(eventId)) } - if (canEdit(timelineEvent, session.myUserId)) { + if (canEdit(timelineEvent, session.myUserId, actionPermissions)) { add(EventSharedAction.Edit(eventId)) } - if (canRedact(timelineEvent, session.myUserId)) { + if (canRedact(timelineEvent, actionPermissions)) { add(EventSharedAction.Redact(eventId, askForReason = informationData.senderId != session.myUserId)) } @@ -270,11 +252,11 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted add(EventSharedAction.Copy(messageContent!!.body)) } - if (timelineEvent.canReact()) { + if (timelineEvent.canReact() && actionPermissions.canReact) { add(EventSharedAction.AddReaction(eventId)) } - if (canQuote(timelineEvent, messageContent)) { + if (canQuote(timelineEvent, messageContent, actionPermissions)) { add(EventSharedAction.Quote(eventId)) } @@ -340,9 +322,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted return false } - private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean { + private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment if (event.root.getClearType() != EventType.MESSAGE) return false + if (!actionPermissions.canSendMessage) return false return when (messageContent?.msgType) { MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_NOTICE, @@ -355,9 +338,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } - private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean { + private fun canQuote(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment if (event.root.getClearType() != EventType.MESSAGE) return false + if (!actionPermissions.canSendMessage) return false return when (messageContent?.msgType) { MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_NOTICE, @@ -369,15 +353,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } } - private fun canRedact(event: TimelineEvent, myUserId: String): Boolean { + private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment if (event.root.getClearType() != EventType.MESSAGE) return false - // TODO if user is admin or moderator - return event.root.senderId == myUserId + return actionPermissions.canRedact } - private fun canRetry(event: TimelineEvent): Boolean { - return event.root.sendState.hasFailed() && event.root.isTextMessage() + private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { + return event.root.sendState.hasFailed() && event.root.isTextMessage() && actionPermissions.canSendMessage } private fun canViewReactions(event: TimelineEvent): Boolean { @@ -387,9 +370,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted return event.annotations?.reactionsSummary?.isNotEmpty() ?: false } - private fun canEdit(event: TimelineEvent, myUserId: String): Boolean { + private fun canEdit(event: TimelineEvent, myUserId: String, actionPermissions: ActionPermissions): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment if (event.root.getClearType() != EventType.MESSAGE) return false + if (!actionPermissions.canSendMessage) return false // TODO if user is admin or moderator val messageContent = event.root.getClearContent().toModel() return event.root.senderId == myUserId && ( 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 419fd673d1..6616025110 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 @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent +import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.prevOrNull import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -38,7 +39,8 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreatio import javax.inject.Inject class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer, - private val avatarSizeProvider: AvatarSizeProvider) { + private val avatarSizeProvider: AvatarSizeProvider, + private val activeSessionHolder: ActiveSessionHolder) { private val collapsedEventIds = linkedSetOf() private val mergeItemCollapseStates = HashMap() @@ -188,7 +190,8 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av }, hasEncryptionEvent = hasEncryption, isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, - readReceiptsCallback = callback + readReceiptsCallback = callback, + currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: "" ) MergedRoomCreationItem_() .id(mergeId) 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 f2ac7018aa..c81a945bc7 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 @@ -54,9 +54,12 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_GUEST_ACCESS, + EventType.STATE_ROOM_WIDGET_LEGACY, + EventType.STATE_ROOM_WIDGET, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, + EventType.STATE_ROOM_POWER_LEVELS, EventType.REACTION, EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_ENCRYPTION -> { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index 9ab48ad5ee..1d178054ac 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -48,6 +48,9 @@ class DisplayableEventFormatter @Inject constructor( val senderName = timelineEvent.senderInfo.disambiguatedDisplayName when (timelineEvent.root.getClearType()) { + EventType.STICKER -> { + return simpleFormat(senderName, stringProvider.getString(R.string.send_a_sticker), appendAuthor) + } EventType.MESSAGE -> { timelineEvent.getLastMessageContent()?.let { messageContent -> when (messageContent.msgType) { 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 cab5b30190..33079a8f33 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 @@ -16,11 +16,13 @@ package im.vector.riotx.features.home.room.detail.timeline.format +import im.vector.matrix.android.api.extensions.orFalse 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.GuestAccess import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent import im.vector.matrix.android.api.session.room.model.RoomAliasesContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomGuestAccessContent @@ -33,7 +35,9 @@ 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.model.create.RoomCreateContent +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.widgets.model.WidgetContent import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent import im.vector.riotx.R @@ -45,6 +49,8 @@ import javax.inject.Inject class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder, private val sp: StringProvider) { + private fun Event.isSentByCurrentUser() = senderId != null && senderId == sessionHolder.getSafeActiveSession()?.myUserId + fun format(timelineEvent: TimelineEvent): CharSequence? { return when (val type = timelineEvent.root.getClearType()) { EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) @@ -57,7 +63,10 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_WIDGET, + EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(type, timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) @@ -78,6 +87,62 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active } } + private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? { + val powerLevelsContent: PowerLevelsContent = event.getClearContent().toModel() ?: return null + val previousPowerLevelsContent: PowerLevelsContent = event.prevContent.toModel() ?: return null + val userIds = HashSet() + userIds.addAll(powerLevelsContent.users.keys) + userIds.addAll(previousPowerLevelsContent.users.keys) + val diffs = ArrayList() + userIds.forEach { userId -> + val from = PowerLevelsHelper(previousPowerLevelsContent).getUserRole(userId) + val to = PowerLevelsHelper(powerLevelsContent).getUserRole(userId) + if (from != to) { + val fromStr = sp.getString(from.res, from.value) + val toStr = sp.getString(to.res, to.value) + val diff = sp.getString(R.string.notice_power_level_diff, userId, fromStr, toStr) + diffs.add(diff) + } + } + if (diffs.isEmpty()) { + return null + } + val diffStr = diffs.joinToString(separator = ", ") + return if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_power_level_changed_by_you, diffStr) + } else { + sp.getString(R.string.notice_power_level_changed, disambiguatedDisplayName, diffStr) + } + } + + private fun formatWidgetEvent(event: Event, disambiguatedDisplayName: String): CharSequence? { + val widgetContent: WidgetContent = event.getClearContent().toModel() ?: return null + val previousWidgetContent: WidgetContent? = event.prevContent.toModel() + return if (widgetContent.isActive()) { + val widgetName = widgetContent.getHumanName() + if (previousWidgetContent?.isActive().orFalse()) { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_widget_modified_by_you, widgetName) + } else { + sp.getString(R.string.notice_widget_modified, disambiguatedDisplayName, widgetName) + } + } else { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_widget_added_by_you, widgetName) + } else { + sp.getString(R.string.notice_widget_added, disambiguatedDisplayName, widgetName) + } + } + } else { + val widgetName = previousWidgetContent?.getHumanName() + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_widget_removed_by_you, widgetName) + } else { + sp.getString(R.string.notice_widget_removed, disambiguatedDisplayName, widgetName) + } + } + } + fun format(event: Event, senderName: String?): CharSequence? { return when (val type = event.getClearType()) { EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName) @@ -88,7 +153,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(senderName) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName) else -> { Timber.v("Type $type not handled by this formatter") null @@ -103,28 +168,54 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active private fun formatRoomCreateEvent(event: Event): CharSequence? { return event.getClearContent().toModel() ?.takeIf { it.creator.isNullOrBlank().not() } - ?.let { sp.getString(R.string.notice_room_created, it.creator) } + ?.let { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_created_by_you) + } else { + sp.getString(R.string.notice_room_created, it.creator) + } + } } private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? { val content = event.getClearContent().toModel() ?: return null return if (content.name.isNullOrBlank()) { - sp.getString(R.string.notice_room_name_removed, senderName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_name_removed_by_you) + } else { + sp.getString(R.string.notice_room_name_removed, senderName) + } } else { - sp.getString(R.string.notice_room_name_changed, senderName, content.name) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_name_changed_by_you, content.name) + } else { + sp.getString(R.string.notice_room_name_changed, senderName, content.name) + } } } - private fun formatRoomTombstoneEvent(senderName: String?): CharSequence? { - return sp.getString(R.string.notice_room_update, senderName) + private fun formatRoomTombstoneEvent(event: Event, senderName: String?): CharSequence? { + return if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_update_by_you) + } else { + sp.getString(R.string.notice_room_update, senderName) + } } private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? { val content = event.getClearContent().toModel() ?: return null return if (content.topic.isNullOrEmpty()) { - sp.getString(R.string.notice_room_topic_removed, senderName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_topic_removed_by_you) + } else { + sp.getString(R.string.notice_room_topic_removed, senderName) + } } else { - sp.getString(R.string.notice_room_topic_changed, senderName, content.topic) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_topic_changed_by_you, content.topic) + } else { + sp.getString(R.string.notice_room_topic_changed, senderName, content.topic) + } } } @@ -137,7 +228,11 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active RoomHistoryVisibility.JOINED -> sp.getString(R.string.notice_room_visibility_joined) RoomHistoryVisibility.WORLD_READABLE -> sp.getString(R.string.notice_room_visibility_world_readable) } - return sp.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility) + return if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_made_future_room_visibility_by_you, formattedVisibility) + } else { + sp.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility) + } } private fun formatCallEvent(type: String, event: Event, senderName: String?): CharSequence? { @@ -146,13 +241,31 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active val content = event.getClearContent().toModel() ?: return null val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO return if (isVideoCall) { - sp.getString(R.string.notice_placed_video_call, senderName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_placed_video_call_by_you) + } else { + sp.getString(R.string.notice_placed_video_call, senderName) + } } else { - sp.getString(R.string.notice_placed_voice_call, senderName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_placed_voice_call_by_you) + } else { + sp.getString(R.string.notice_placed_voice_call, senderName) + } } } - EventType.CALL_ANSWER -> sp.getString(R.string.notice_answered_call, senderName) - EventType.CALL_HANGUP -> sp.getString(R.string.notice_ended_call, senderName) + EventType.CALL_ANSWER -> + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_answered_call_by_you) + } else { + sp.getString(R.string.notice_answered_call, senderName) + } + EventType.CALL_HANGUP -> + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_ended_call_by_you) + } else { + sp.getString(R.string.notice_ended_call, senderName) + } else -> null } } @@ -175,15 +288,29 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active val addedAliases = eventContent?.aliases.orEmpty() - prevEventContent?.aliases.orEmpty() val removedAliases = prevEventContent?.aliases.orEmpty() - eventContent?.aliases.orEmpty() - return if (addedAliases.isNotEmpty() && removedAliases.isNotEmpty()) { - sp.getString(R.string.notice_room_aliases_added_and_removed, senderName, addedAliases.joinToString(), removedAliases.joinToString()) - } else if (addedAliases.isNotEmpty()) { - sp.getQuantityString(R.plurals.notice_room_aliases_added, addedAliases.size, senderName, addedAliases.joinToString()) - } else if (removedAliases.isNotEmpty()) { - sp.getQuantityString(R.plurals.notice_room_aliases_removed, removedAliases.size, senderName, removedAliases.joinToString()) - } else { - Timber.w("Alias event without any change...") - null + return when { + addedAliases.isNotEmpty() && removedAliases.isNotEmpty() -> + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_aliases_added_and_removed_by_you, addedAliases.joinToString(), removedAliases.joinToString()) + } else { + sp.getString(R.string.notice_room_aliases_added_and_removed, senderName, addedAliases.joinToString(), removedAliases.joinToString()) + } + addedAliases.isNotEmpty() -> + if (event.isSentByCurrentUser()) { + sp.getQuantityString(R.plurals.notice_room_aliases_added_by_you, addedAliases.size, addedAliases.joinToString()) + } else { + sp.getQuantityString(R.plurals.notice_room_aliases_added, addedAliases.size, senderName, addedAliases.joinToString()) + } + removedAliases.isNotEmpty() -> + if (event.isSentByCurrentUser()) { + sp.getQuantityString(R.plurals.notice_room_aliases_removed_by_you, removedAliases.size, removedAliases.joinToString()) + } else { + sp.getQuantityString(R.plurals.notice_room_aliases_removed, removedAliases.size, senderName, removedAliases.joinToString()) + } + else -> { + Timber.w("Alias event without any change...") + null + } } } @@ -192,25 +319,54 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active val canonicalAlias = eventContent?.canonicalAlias return canonicalAlias ?.takeIf { it.isNotBlank() } - ?.let { sp.getString(R.string.notice_room_canonical_alias_set, senderName, it) } - ?: sp.getString(R.string.notice_room_canonical_alias_unset, senderName) + ?.let { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_canonical_alias_set_by_you, it) + } else { + sp.getString(R.string.notice_room_canonical_alias_set, senderName, it) + } + } + ?: if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_canonical_alias_unset_by_you) + } else { + sp.getString(R.string.notice_room_canonical_alias_unset, senderName) + } } private fun formatRoomGuestAccessEvent(event: Event, senderName: String?): String? { val eventContent: RoomGuestAccessContent? = event.getClearContent().toModel() return when (eventContent?.guestAccess) { - GuestAccess.CanJoin -> sp.getString(R.string.notice_room_guest_access_can_join, senderName) - GuestAccess.Forbidden -> sp.getString(R.string.notice_room_guest_access_forbidden, senderName) + GuestAccess.CanJoin -> + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_guest_access_can_join_by_you) + } else { + sp.getString(R.string.notice_room_guest_access_can_join, senderName) + } + GuestAccess.Forbidden -> + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_guest_access_forbidden_by_you) + } else { + sp.getString(R.string.notice_room_guest_access_forbidden, senderName) + } else -> null } } private fun formatRoomEncryptionEvent(event: Event, senderName: String?): CharSequence? { val content = event.content.toModel() ?: return null - return if (content.algorithm == MXCRYPTO_ALGORITHM_MEGOLM) { - sp.getString(R.string.notice_end_to_end_ok, senderName) - } else { - sp.getString(R.string.notice_end_to_end_unknown_algorithm, senderName, content.algorithm) + return when (content.algorithm) { + MXCRYPTO_ALGORITHM_MEGOLM -> + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_end_to_end_ok_by_you) + } else { + sp.getString(R.string.notice_end_to_end_ok, senderName) + } + else -> + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_end_to_end_unknown_algorithm_by_you, content.algorithm) + } else { + sp.getString(R.string.notice_end_to_end_unknown_algorithm, senderName, content.algorithm) + } } } @@ -220,11 +376,23 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active if (eventContent?.displayName != prevEventContent?.displayName) { val displayNameText = when { prevEventContent?.displayName.isNullOrEmpty() -> - sp.getString(R.string.notice_display_name_set, event.senderId, eventContent?.displayName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_display_name_set_by_you, eventContent?.displayName) + } else { + sp.getString(R.string.notice_display_name_set, event.senderId, eventContent?.displayName) + } eventContent?.displayName.isNullOrEmpty() -> - sp.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_display_name_removed_by_you, prevEventContent?.displayName) + } else { + sp.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName) + } else -> - sp.getString(R.string.notice_display_name_changed_from, event.senderId, prevEventContent?.displayName, eventContent?.displayName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_display_name_changed_from_by_you, prevEventContent?.displayName, eventContent?.displayName) + } else { + sp.getString(R.string.notice_display_name_changed_from, event.senderId, prevEventContent?.displayName, eventContent?.displayName) + } } displayText.append(displayNameText) } @@ -234,13 +402,21 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active displayText.append(" ") sp.getString(R.string.notice_avatar_changed_too) } else { - sp.getString(R.string.notice_avatar_url_changed, senderName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_avatar_url_changed_by_you) + } else { + sp.getString(R.string.notice_avatar_url_changed, senderName) + } } displayText.append(displayAvatarText) } if (displayText.isEmpty()) { displayText.append( - sp.getString(R.string.notice_member_no_changes, senderName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_member_no_changes_by_you) + } else { + sp.getString(R.string.notice_member_no_changes, senderName) + } ) } return displayText.toString() @@ -257,62 +433,133 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey val threePidDisplayName = eventContent.thirdPartyInvite?.displayName ?: "" eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_third_party_registered_invite_with_reason, userWhoHasAccepted, threePidDisplayName, reason) - } ?: sp.getString(R.string.notice_room_third_party_registered_invite, userWhoHasAccepted, threePidDisplayName) + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_third_party_registered_invite_with_reason_by_you, threePidDisplayName, reason) + } else { + sp.getString(R.string.notice_room_third_party_registered_invite_with_reason, userWhoHasAccepted, threePidDisplayName, reason) + } + } ?: if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_third_party_registered_invite_by_you, threePidDisplayName) + } else { + sp.getString(R.string.notice_room_third_party_registered_invite, userWhoHasAccepted, threePidDisplayName) + } } event.stateKey == selfUserId -> eventContent.safeReason?.let { reason -> sp.getString(R.string.notice_room_invite_you_with_reason, senderDisplayName, reason) } ?: sp.getString(R.string.notice_room_invite_you, senderDisplayName) event.stateKey.isNullOrEmpty() -> - eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_invite_no_invitee_with_reason, senderDisplayName, reason) - } ?: sp.getString(R.string.notice_room_invite_no_invitee, senderDisplayName) + if (event.isSentByCurrentUser()) { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_invite_no_invitee_with_reason_by_you, reason) + } ?: sp.getString(R.string.notice_room_invite_no_invitee_by_you) + } else { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_invite_no_invitee_with_reason, senderDisplayName, reason) + } ?: sp.getString(R.string.notice_room_invite_no_invitee, senderDisplayName) + } else -> - eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_invite_with_reason, senderDisplayName, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName) + if (event.isSentByCurrentUser()) { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_invite_with_reason_by_you, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_invite_by_you, targetDisplayName) + } else { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_invite_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName) + } } } Membership.JOIN -> - eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_join_with_reason, senderDisplayName, reason) - } ?: sp.getString(R.string.notice_room_join, senderDisplayName) + if (event.isSentByCurrentUser()) { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_join_with_reason_by_you, reason) + } ?: sp.getString(R.string.notice_room_join_by_you) + } else { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_join_with_reason, senderDisplayName, reason) + } ?: sp.getString(R.string.notice_room_join, senderDisplayName) + } Membership.LEAVE -> // 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked if (event.senderId == event.stateKey) { - if (prevEventContent?.membership == Membership.INVITE) { - eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_reject_with_reason, senderDisplayName, reason) - } ?: sp.getString(R.string.notice_room_reject, senderDisplayName) - } else { - eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_leave_with_reason, senderDisplayName, reason) - } ?: sp.getString(R.string.notice_room_leave, senderDisplayName) + when (prevEventContent?.membership) { + Membership.INVITE -> + if (event.isSentByCurrentUser()) { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_reject_with_reason_by_you, reason) + } ?: sp.getString(R.string.notice_room_reject_by_you) + } else { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_reject_with_reason, senderDisplayName, reason) + } ?: sp.getString(R.string.notice_room_reject, senderDisplayName) + } + else -> + if (event.isSentByCurrentUser()) { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_leave_with_reason_by_you, reason) + } ?: sp.getString(R.string.notice_room_leave_by_you) + } else { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_leave_with_reason, senderDisplayName, reason) + } ?: sp.getString(R.string.notice_room_leave, senderDisplayName) + } } - } else if (prevEventContent?.membership == Membership.INVITE) { + } else { + when (prevEventContent?.membership) { + Membership.INVITE -> + if (event.isSentByCurrentUser()) { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_withdraw_with_reason_by_you, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_withdraw_by_you, targetDisplayName) + } else { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_withdraw_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName) + } + Membership.JOIN -> + if (event.isSentByCurrentUser()) { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_kick_with_reason_by_you, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_kick_by_you, targetDisplayName) + } else { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) + } + Membership.BAN -> + if (event.isSentByCurrentUser()) { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_unban_with_reason_by_you, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_unban_by_you, targetDisplayName) + } else { + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_unban_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName) + } + else -> null + } + } + Membership.BAN -> + if (event.isSentByCurrentUser()) { + eventContent.safeReason?.let { + sp.getString(R.string.notice_room_ban_with_reason_by_you, targetDisplayName, it) + } ?: sp.getString(R.string.notice_room_ban_by_you, targetDisplayName) + } else { + eventContent.safeReason?.let { + sp.getString(R.string.notice_room_ban_with_reason, senderDisplayName, targetDisplayName, it) + } ?: sp.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName) + } + Membership.KNOCK -> + if (event.isSentByCurrentUser()) { eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_withdraw_with_reason, senderDisplayName, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName) - } else if (prevEventContent?.membership == Membership.JOIN) { + sp.getString(R.string.notice_room_kick_with_reason_by_you, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_kick_by_you, targetDisplayName) + } else { eventContent.safeReason?.let { reason -> sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason) } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) - } else if (prevEventContent?.membership == Membership.BAN) { - eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_unban_with_reason, senderDisplayName, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName) - } else { - null } - Membership.BAN -> - eventContent.safeReason?.let { - sp.getString(R.string.notice_room_ban_with_reason, senderDisplayName, targetDisplayName, it) - } ?: sp.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName) - Membership.KNOCK -> - eventContent.safeReason?.let { reason -> - sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason) - } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) else -> null } } @@ -320,8 +567,18 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active private fun formatJoinRulesEvent(event: Event, senderName: String?): CharSequence? { val content = event.getClearContent().toModel() ?: return null return when (content.joinRules) { - RoomJoinRules.INVITE -> sp.getString(R.string.room_join_rules_invite, senderName) - RoomJoinRules.PUBLIC -> sp.getString(R.string.room_join_rules_public, senderName) + RoomJoinRules.INVITE -> + if (event.isSentByCurrentUser()) { + sp.getString(R.string.room_join_rules_invite_by_you) + } else { + sp.getString(R.string.room_join_rules_invite, senderName) + } + RoomJoinRules.PUBLIC -> + if (event.isSentByCurrentUser()) { + sp.getString(R.string.room_join_rules_public_by_you) + } else { + sp.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/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 9a912b5af3..c36dc5af9b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -158,7 +158,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses } } } else { - if (EventType.isStateEvent(event.root.type)) { + if (event.root.isStateEvent()) { // Do not warn for state event, they are always in clear E2EDecoration.NONE } else { 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 daf0100bbb..882d8e8869 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 @@ -24,12 +24,15 @@ object TimelineDisplayableEvents { val DISPLAYABLE_TYPES = listOf( EventType.MESSAGE, + EventType.STATE_ROOM_WIDGET_LEGACY, + EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_HISTORY_VISIBILITY, + EventType.STATE_ROOM_POWER_LEVELS, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt index 3985b3856b..cf044f8a37 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -46,8 +46,12 @@ abstract class MergedRoomCreationItem : BasedMergedItem Unit, - val hasEncryptionEvent : Boolean, + val currentUserId: String, + val hasEncryptionEvent: Boolean, val isEncryptionAlgorithmSecure: Boolean ) : BasedMergedItem.Attributes } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetController.kt new file mode 100644 index 0000000000..1fe85d0264 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetController.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.widget + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.session.widgets.model.Widget +import javax.inject.Inject + +/** + * Epoxy controller for room widgets list + */ +class RoomWidgetController @Inject constructor() : TypedEpoxyController>() { + + var listener: Listener? = null + + override fun buildModels(widget: List) { + widget.forEach { + RoomWidgetItem_() + .id(it.widgetId) + .widget(it) + .widgetClicked { listener?.didSelectWidget(it) } + .addTo(this) + } + } + + interface Listener { + fun didSelectWidget(widget: Widget) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetItem.kt new file mode 100644 index 0000000000..8b7d32bb99 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetItem.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.widget + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.ClickListener +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.onClick + +@EpoxyModelClass(layout = R.layout.item_room_widget) +abstract class RoomWidgetItem : EpoxyModelWithHolder() { + + @EpoxyAttribute lateinit var widget: Widget + @EpoxyAttribute var widgetClicked: ClickListener? = null + + override fun bind(holder: Holder) { + holder.widgetName.text = widget.name + holder.view.onClick(widgetClicked) + } + + class Holder : VectorEpoxyHolder() { + val widgetName by bind(R.id.roomWidgetName) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetsBannerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetsBannerView.kt new file mode 100644 index 0000000000..48a5c20558 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetsBannerView.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.RelativeLayout +import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.riotx.R +import kotlinx.android.synthetic.main.view_room_widgets_banner.view.* + +class RoomWidgetsBannerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { + + interface Callback { + fun onViewWidgetsClicked() + } + + var callback: Callback? = null + + init { + setupView() + } + + private fun setupView() { + inflate(context, R.layout.view_room_widgets_banner, this) + setBackgroundResource(R.drawable.bg_active_widgets_banner) + setOnClickListener { + callback?.onViewWidgetsClicked() + } + } + + fun render(widgets: List?) { + if (widgets.isNullOrEmpty()) { + visibility = View.GONE + } else { + visibility = View.VISIBLE + activeWidgetsLabel.text = context.resources.getQuantityString(R.plurals.active_widgets, widgets.size, widgets.size) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt new file mode 100644 index 0000000000..832d4f836f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail.widget + +import android.os.Bundle +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.features.home.room.detail.RoomDetailViewModel +import im.vector.riotx.features.home.room.detail.RoomDetailViewState +import im.vector.riotx.features.navigation.Navigator +import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.* +import javax.inject.Inject + +/** + * Bottom sheet displaying active widgets in a room + */ +class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidgetController.Listener { + + @Inject lateinit var epoxyController: RoomWidgetController + @Inject lateinit var colorProvider: ColorProvider + @Inject lateinit var navigator: Navigator + + @BindView(R.id.bottomSheetRecyclerView) + lateinit var recyclerView: RecyclerView + + private val roomDetailViewModel: RoomDetailViewModel by parentFragmentViewModel() + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getLayoutResId() = R.layout.bottom_sheet_generic_list_with_title + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + recyclerView.configureWith(epoxyController, hasFixedSize = false) + bottomSheetTitle.text = getString(R.string.active_widgets_title) + bottomSheetTitle.textSize = 20f + bottomSheetTitle.setTextColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + epoxyController.listener = this + roomDetailViewModel.asyncSubscribe(this, RoomDetailViewState::activeRoomWidgets) { + epoxyController.setData(it) + } + } + + override fun onDestroyView() { + recyclerView.cleanup() + epoxyController.listener = null + super.onDestroyView() + } + + override fun didSelectWidget(widget: Widget) = withState(roomDetailViewModel) { + navigator.openRoomWidget(requireContext(), it.roomId, widget) + dismiss() + } + + companion object { + fun newInstance(): RoomWidgetsBottomSheet { + return RoomWidgetsBottomSheet() + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt index 9db7374169..44ae10c85d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt @@ -27,6 +27,7 @@ sealed class RoomListAction : VectorViewModelAction { 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 ToggleFavorite(val roomId: String) : 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/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index 1484e8009b..b31117f18f 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 @@ -32,6 +32,7 @@ import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState 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 @@ -71,6 +72,7 @@ class RoomListFragment @Inject constructor( private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel private val roomListParams: RoomListParams by args() private val roomListViewModel: RoomListViewModel by fragmentViewModel() + private lateinit var stateRestorer: LayoutManagerStateRestorer override fun getLayoutResId() = R.layout.fragment_room_list @@ -99,7 +101,6 @@ class RoomListFragment @Inject constructor( setupCreateRoomButton() setupRecyclerView() sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) - roomListViewModel.subscribe { renderState(it) } roomListViewModel.observeViewEvents { when (it) { is RoomListViewEvents.Loading -> showLoading(it.message) @@ -126,6 +127,7 @@ class RoomListFragment @Inject constructor( modelBuildListener = null roomListView.cleanup() roomController.listener = null + stateRestorer.clear() createChatFabMenu.listener = null super.onDestroyView() } @@ -190,7 +192,7 @@ class RoomListFragment @Inject constructor( private fun setupRecyclerView() { val layoutManager = LinearLayoutManager(context) - val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() + stateRestorer = LayoutManagerStateRestorer(layoutManager).register() roomListView.layoutManager = layoutManager roomListView.itemAnimator = RoomListAnimator() roomListView.setRecycledViewPool(sharedViewPool) @@ -230,6 +232,9 @@ class RoomListFragment @Inject constructor( is RoomListQuickActionsSharedAction.Settings -> { navigator.openRoomProfile(requireActivity(), quickAction.roomId) } + is RoomListQuickActionsSharedAction.Favorite -> { + roomListViewModel.handle(RoomListAction.ToggleFavorite(quickAction.roomId)) + } is RoomListQuickActionsSharedAction.Leave -> { AlertDialog.Builder(requireContext()) .setTitle(R.string.room_participants_leave_prompt_title) @@ -239,11 +244,12 @@ class RoomListFragment @Inject constructor( } .setNegativeButton(R.string.cancel, null) .show() + Unit } - } + }.exhaustive } - private fun renderState(state: RoomListViewState) { + override fun invalidate() = withState(roomListViewModel) { state -> when (state.asyncFilteredRooms) { is Incomplete -> renderLoading() is Success -> renderSuccess(state) 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 fb1f88af3d..cac8a2b91b 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 @@ -67,6 +67,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() is RoomListAction.LeaveRoom -> handleLeaveRoom(action) is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) + is RoomListAction.ToggleFavorite -> handleToggleFavorite(action) }.exhaustive } @@ -205,6 +206,22 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, }) } + private fun handleToggleFavorite(action: RoomListAction.ToggleFavorite) { + session.getRoom(action.roomId)?.let { + val callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + _viewEvents.post(RoomListViewEvents.Failure(failure)) + } + } + if (it.roomSummary()?.isFavorite == false) { + // Set favorite tag. We do not handle the order for the moment + it.addTag(RoomTag.ROOM_TAG_FAVOURITE, 0.5, callback) + } else { + it.deleteTag(RoomTag.ROOM_TAG_FAVOURITE, callback) + } + } + } + private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) { _viewEvents.post(RoomListViewEvents.Loading(null)) session.getRoom(action.roomId)?.leave(null, object : MatrixCallback { 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 index 6148b3c725..32dc038222 100644 --- 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 @@ -86,7 +86,11 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R override fun didSelectMenuAction(quickAction: RoomListQuickActionsSharedAction) { sharedActionViewModel.post(quickAction) - dismiss() + // Do not dismiss for all the actions + when (quickAction) { + is RoomListQuickActionsSharedAction.Favorite -> Unit + else -> dismiss() + } } companion object { 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 index 132da9341b..74475406ed 100644 --- 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 @@ -22,14 +22,17 @@ import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetActionItem import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem import im.vector.riotx.core.epoxy.dividerItem +import im.vector.riotx.core.resources.StringProvider 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() { +class RoomListQuickActionsEpoxyController @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider +) : TypedEpoxyController() { var listener: Listener? = null @@ -38,12 +41,15 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar val showAll = state.mode == RoomListActionsArgs.Mode.FULL if (showAll) { - // Preview + // Preview, favorite, settings bottomSheetRoomPreviewItem { id("room_preview") avatarRenderer(avatarRenderer) matrixItem(roomSummary.toMatrixItem()) + stringProvider(stringProvider) + izFavorite(roomSummary.isFavorite) settingsClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Settings(roomSummary.roomId)) } + favoriteClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Favorite(roomSummary.roomId)) } } // Notifications @@ -73,8 +79,7 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar 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 + else -> false } return bottomSheetActionItem { id("action_$index") 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 index ca006ddd7d..5da15bf6a3 100644 --- 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 @@ -52,6 +52,10 @@ sealed class RoomListQuickActionsSharedAction( R.drawable.ic_room_actions_settings ) + data class Favorite(val roomId: String) : RoomListQuickActionsSharedAction( + R.string.room_list_quick_actions_favorite_add, + R.drawable.ic_star_24dp) + data class Leave(val roomId: String) : RoomListQuickActionsSharedAction( R.string.room_list_quick_actions_leave, R.drawable.ic_room_actions_leave, diff --git a/vector/src/main/java/im/vector/riotx/features/login/Config.kt b/vector/src/main/java/im/vector/riotx/features/login/Config.kt index e35923f5b0..66db981b1c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/Config.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/Config.kt @@ -16,4 +16,7 @@ package im.vector.riotx.features.login -const val MODULAR_LINK = "https://modular.im/?utm_source=riot-x-android&utm_medium=native&utm_campaign=riot-x-android-authentication" +const val MODULAR_LINK = "https://modular.im/services/matrix-hosting-riot" + + "?utm_source=riot-x-android" + + "&utm_medium=native" + + "&utm_campaign=riot-x-android-authentication" diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 3403760136..afd27f04a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -24,6 +24,7 @@ sealed class LoginAction : VectorViewModelAction { data class UpdateServerType(val serverType: ServerType) : LoginAction() data class UpdateHomeServer(val homeServerUrl: String) : LoginAction() data class UpdateSignMode(val signMode: SignMode) : LoginAction() + data class LoginWithToken(val loginToken: String) : LoginAction() data class WebLoginSuccess(val credentials: Credentials) : LoginAction() data class InitWith(val loginConfig: LoginConfig) : LoginAction() data class ResetPassword(val email: String, val newPassword: String) : 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 99d8da490d..13bde075a6 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 @@ -33,6 +33,7 @@ import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.Stage +import im.vector.matrix.android.api.extensions.tryThis import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE @@ -45,7 +46,6 @@ import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.login.terms.LoginTermsFragment import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument import im.vector.riotx.features.login.terms.toLocalizedLoginTerms -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_login.* import javax.inject.Inject @@ -103,13 +103,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { updateWithState(it) } - loginViewModel.viewEvents - .observe() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - handleLoginViewEvents(it) - } - .disposeOnDestroy() + loginViewModel.observeViewEvents { handleLoginViewEvents(it) } } protected open fun addFirstFragment() { @@ -162,7 +156,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { is LoginViewEvents.OnSignModeSelected -> onSignModeSelected() is LoginViewEvents.OnLoginFlowRetrieved -> addFragmentToBackstack(R.id.loginFragmentContainer, - LoginSignUpSignInSelectionFragment::class.java, + if (loginViewEvents.isSso) { + LoginSignUpSignInSsoFragment::class.java + } else { + LoginSignUpSignInSelectionFragment::class.java + }, option = commonOption) is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) is LoginViewEvents.OnForgetPasswordClicked -> @@ -246,16 +244,14 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { SignMode.SignIn -> { // It depends on the LoginMode when (state.loginMode) { - LoginMode.Unknown -> error("Developer error") + LoginMode.Unknown, + LoginMode.Sso -> error("Developer error") LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java, tag = FRAGMENT_LOGIN_TAG, option = commonOption) - LoginMode.Sso -> addFragmentToBackstack(R.id.loginFragmentContainer, - LoginWebFragment::class.java, - option = commonOption) LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) - } + }.exhaustive } SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java, @@ -264,6 +260,17 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { }.exhaustive } + /** + * Handle the SSO redirection here + */ + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + intent?.data + ?.let { tryThis { it.getQueryParameter("loginToken") } } + ?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) } + } + private fun onRegistrationStageNotSupported() { AlertDialog.Builder(this) .setTitle(R.string.app_name) 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 c2bd02b817..eaf0a3cc78 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 @@ -21,6 +21,7 @@ import android.os.Bundle import android.view.View import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants +import androidx.core.text.isDigitsOnly import androidx.core.view.isVisible import butterknife.OnClick import com.airbnb.mvrx.Fail @@ -53,6 +54,9 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { private var passwordShown = false private var isSignupMode = false + // Temporary patch for https://github.com/vector-im/riotX-android/issues/1410, + // waiting for https://github.com/matrix-org/synapse/issues/7576 + private var isNumericOnlyUserIdForbidden = false override fun getLayoutResId() = R.layout.fragment_login @@ -101,6 +105,10 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginFieldTil.error = getString(if (isSignupMode) R.string.error_empty_field_choose_user_name else R.string.error_empty_field_enter_user_name) error++ } + if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) { + loginFieldTil.error = "The homeserver does not accept username with only digits." + error++ + } if (password.isEmpty()) { passwordFieldTil.error = getString(if (isSignupMode) R.string.error_empty_field_choose_password else R.string.error_empty_field_your_password) error++ @@ -227,6 +235,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { override fun updateWithState(state: LoginViewState) { isSignupMode = state.signMode == SignMode.SignUp + isNumericOnlyUserIdForbidden = state.serverType == ServerType.MatrixOrg setupUi(state) setupAutoFill(state) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 0e234d3da8..79c5c7d024 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -21,7 +21,7 @@ import android.view.View import butterknife.OnClick import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.utils.openUrlInExternalBrowser +import im.vector.riotx.core.utils.openUrlInChromeCustomTab import kotlinx.android.synthetic.main.fragment_login_server_selection.* import me.gujun.android.span.span import javax.inject.Inject @@ -55,8 +55,8 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment } @OnClick(R.id.loginServerChoiceModularLearnMore) - fun learMore() { - openUrlInExternalBrowser(requireActivity(), MODULAR_LINK) + fun learnMore() { + openUrlInChromeCustomTab(requireActivity(), null, MODULAR_LINK) } @OnClick(R.id.loginServerChoiceMatrixOrg) @@ -113,7 +113,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment if (state.loginMode != LoginMode.Unknown) { // LoginFlow for matrix.org has been retrieved - loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso))) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index 6f95847b20..cb90ef2397 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -26,7 +26,7 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.utils.ensureProtocol -import im.vector.riotx.core.utils.openUrlInExternalBrowser +import im.vector.riotx.core.utils.openUrlInChromeCustomTab import kotlinx.android.synthetic.main.fragment_login_server_url_form.* import javax.inject.Inject @@ -84,7 +84,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() @OnClick(R.id.loginServerUrlFormLearnMore) fun learnMore() { - openUrlInExternalBrowser(requireActivity(), MODULAR_LINK) + openUrlInChromeCustomTab(requireActivity(), null, MODULAR_LINK) } override fun resetViewModel() { @@ -124,7 +124,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() if (state.loginMode != LoginMode.Unknown) { // The home server url is valid - loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso))) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index f09053c883..427ad99b41 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -26,13 +26,11 @@ import javax.inject.Inject /** * In this screen, the user is asked to sign up or to sign in to the homeserver */ -class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { +open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection - private var isSsoSignIn: Boolean = false - - private fun setupUi(state: LoginViewState) { + protected fun setupUi(state: LoginViewState) { when (state.serverType) { ServerType.MatrixOrg -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) @@ -54,25 +52,14 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr } } - private fun setupButtons(state: LoginViewState) { - isSsoSignIn = state.loginMode == LoginMode.Sso - - if (isSsoSignIn) { - loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) - loginSignupSigninSignIn.isVisible = false - } else { - loginSignupSigninSubmit.text = getString(R.string.login_signup) - loginSignupSigninSignIn.isVisible = true - } + private fun setupButtons() { + loginSignupSigninSubmit.text = getString(R.string.login_signup) + loginSignupSigninSignIn.isVisible = true } @OnClick(R.id.loginSignupSigninSubmit) - fun signUp() { - if (isSsoSignIn) { - signIn() - } else { - loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) - } + open fun submit() { + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) } @OnClick(R.id.loginSignupSigninSignIn) @@ -86,6 +73,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr override fun updateWithState(state: LoginViewState) { setupUi(state) - setupButtons(state) + setupButtons() } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSsoFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSsoFragment.kt new file mode 100644 index 0000000000..538e8be675 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSsoFragment.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.login + +import android.content.ComponentName +import android.net.Uri +import androidx.browser.customtabs.CustomTabsClient +import androidx.browser.customtabs.CustomTabsServiceConnection +import androidx.browser.customtabs.CustomTabsSession +import androidx.core.view.isVisible +import im.vector.riotx.R +import im.vector.riotx.core.utils.openUrlInChromeCustomTab +import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* +import javax.inject.Inject + +/** + * In this screen, the user is asked to sign up or to sign in using SSO + * This Fragment binds a CustomTabsServiceConnection if available, then prefetch the SSO url, as it will be likely to be opened. + */ +open class LoginSignUpSignInSsoFragment @Inject constructor() : LoginSignUpSignInSelectionFragment() { + + private var ssoUrl: String? = null + private var customTabsServiceConnection: CustomTabsServiceConnection? = null + private var customTabsClient: CustomTabsClient? = null + private var customTabsSession: CustomTabsSession? = null + + override fun onStart() { + super.onStart() + + val packageName = CustomTabsClient.getPackageName(requireContext(), null) + + // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device + if (packageName != null) { + customTabsServiceConnection = object : CustomTabsServiceConnection() { + override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { + customTabsClient = client + .also { it.warmup(0L) } + } + + override fun onServiceDisconnected(name: ComponentName?) { + } + } + .also { + CustomTabsClient.bindCustomTabsService( + requireContext(), + // Despite the API, packageName cannot be null + packageName, + it + ) + } + } + } + + private fun prefetchUrl(url: String) { + if (ssoUrl != null) return + + ssoUrl = url + if (customTabsSession == null) { + customTabsSession = customTabsClient?.newSession(null) + } + + customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null) + } + + override fun onStop() { + super.onStop() + customTabsServiceConnection?.let { requireContext().unbindService(it) } + customTabsServiceConnection = null + } + + private fun setupButtons() { + loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) + loginSignupSigninSignIn.isVisible = false + } + + override fun submit() { + ssoUrl?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) } + } + + override fun updateWithState(state: LoginViewState) { + setupUi(state) + setupButtons() + prefetchUrl(state.getSsoUrl()) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt index c7c2ee6273..9b69ba8a4f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt @@ -34,7 +34,7 @@ sealed class LoginViewEvents : VectorViewEvents { object OpenServerSelection : LoginViewEvents() object OnServerSelectionDone : LoginViewEvents() - object OnLoginFlowRetrieved : LoginViewEvents() + data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents() object OnSignModeSelected : LoginViewEvents() object OnForgetPasswordClicked : LoginViewEvents() object OnResetPasswordSendThreePidDone : LoginViewEvents() 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 81dcfcea9f..f15cb801ed 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 @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.RegistrationResult @@ -40,7 +41,6 @@ import im.vector.matrix.android.api.auth.registration.Stage import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder @@ -110,6 +110,7 @@ class LoginViewModel @AssistedInject constructor( is LoginAction.InitWith -> handleInitWith(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.LoginOrRegister -> handleLoginOrRegister(action) + is LoginAction.LoginWithToken -> handleLoginWithToken(action) is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) is LoginAction.ResetPassword -> handleResetPassword(action) is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() @@ -120,6 +121,41 @@ class LoginViewModel @AssistedInject constructor( }.exhaustive } + private fun handleLoginWithToken(action: LoginAction.LoginWithToken) { + val safeLoginWizard = loginWizard + + if (safeLoginWizard == null) { + setState { + copy( + asyncLoginAction = Fail(Throwable("Bad configuration")) + ) + } + } else { + setState { + copy( + asyncLoginAction = Loading() + ) + } + + currentTask = safeLoginWizard.loginWithToken( + action.loginToken, + object : MatrixCallback { + override fun onSuccess(data: Session) { + onSessionCreated(data) + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(LoginViewEvents.Failure(failure)) + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) + } + } + private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) { setState { copy( @@ -635,9 +671,9 @@ class LoginViewModel @AssistedInject constructor( is LoginFlowResult.Success -> { val loginMode = when { // SSO login is taken first - data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso - data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { @@ -648,7 +684,7 @@ class LoginViewModel @AssistedInject constructor( asyncHomeServerLoginFlowRequest = Uninitialized, homeServerUrl = data.homeServerUrl, loginMode = loginMode, - loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList() + loginModeSupportedTypes = data.supportedLoginTypes.toList() ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt index 3f81fa8f4b..944d1f7d82 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -22,6 +22,9 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.PersistState import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.auth.SSO_REDIRECT_PATH +import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM +import im.vector.riotx.core.extensions.appendParamToUrl data class LoginViewState( val asyncLoginAction: Async = Uninitialized, @@ -64,4 +67,22 @@ data class LoginViewState( fun isUserLogged(): Boolean { return asyncLoginAction is Success } + + fun getSsoUrl(): String { + return buildString { + append(homeServerUrl?.trim { it == '/' }) + append(SSO_REDIRECT_PATH) + // Set a redirect url we will intercept later + appendParamToUrl(SSO_REDIRECT_URL_PARAM, RIOTX_REDIRECT_URL) + deviceId?.takeIf { it.isNotBlank() }?.let { + // But https://github.com/matrix-org/synapse/issues/5755 + appendParamToUrl("device_id", it) + } + } + } + + companion object { + // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string + private const val RIOTX_REDIRECT_URL = "riotx://riotx" + } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index cf3b39ebb0..62431647c0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -33,8 +33,6 @@ import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.activityViewModel import im.vector.matrix.android.api.auth.LOGIN_FALLBACK_PATH import im.vector.matrix.android.api.auth.REGISTER_FALLBACK_PATH -import im.vector.matrix.android.api.auth.SSO_FALLBACK_PATH -import im.vector.matrix.android.api.auth.SSO_REDIRECT_URL_PARAM import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R @@ -48,7 +46,7 @@ import java.net.URLDecoder import javax.inject.Inject /** - * This screen is displayed for SSO login and also when the application does not support login flow or registration flow + * This screen is displayed when the application does not support login flow or registration flow * of the homeserver, as a fallback to login or to create an account */ class LoginWebFragment @Inject constructor( @@ -128,17 +126,7 @@ class LoginWebFragment @Inject constructor( val url = buildString { append(state.homeServerUrl?.trim { it == '/' }) if (state.signMode == SignMode.SignIn) { - if (state.loginMode == LoginMode.Sso) { - append(SSO_FALLBACK_PATH) - // We do not want to deal with the result, so let the fallback login page to handle it for us - appendParamToUrl(SSO_REDIRECT_URL_PARAM, - buildString { - append(state.homeServerUrl?.trim { it == '/' }) - append(LOGIN_FALLBACK_PATH) - }) - } else { - append(LOGIN_FALLBACK_PATH) - } + append(LOGIN_FALLBACK_PATH) state.deviceId?.takeIf { it.isNotBlank() }?.let { // But https://github.com/matrix-org/synapse/issues/5755 appendParamToUrl("device_id", it) @@ -226,7 +214,9 @@ class LoginWebFragment @Inject constructor( * @return */ override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { - if (null != url && url.startsWith("js:")) { + if (url == null) return super.shouldOverrideUrlLoading(view, url as String?) + + if (url.startsWith("js:")) { var json = url.substring(3) var javascriptResponse: JavascriptResponse? = null 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 db12bcf61b..6a0094520a 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 @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerif import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.error.fatalError @@ -45,6 +46,7 @@ import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.debug.DebugMenuActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailArgs +import im.vector.riotx.features.home.room.detail.sticker.StickerPickerConstants import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.media.BigImageViewerActivity @@ -62,6 +64,8 @@ import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.terms.ReviewTermsActivity +import im.vector.riotx.features.widgets.WidgetActivity +import im.vector.riotx.features.widgets.WidgetArgsBuilder import javax.inject.Inject import javax.inject.Singleton @@ -69,6 +73,7 @@ import javax.inject.Singleton class DefaultNavigator @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val vectorPreferences: VectorPreferences, + private val widgetArgsBuilder: WidgetArgsBuilder, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider ) : Navigator { @@ -222,6 +227,22 @@ class DefaultNavigator @Inject constructor( fragment.startActivityForResult(intent, requestCode) } + override fun openStickerPicker(fragment: Fragment, roomId: String, widget: Widget, requestCode: Int) { + val widgetArgs = widgetArgsBuilder.buildStickerPickerArgs(roomId, widget) + val intent = WidgetActivity.newIntent(fragment.requireContext(), widgetArgs) + fragment.startActivityForResult(intent, StickerPickerConstants.STICKER_PICKER_REQUEST_CODE) + } + + override fun openIntegrationManager(context: Context, roomId: String, integId: String?, screen: String?) { + val widgetArgs = widgetArgsBuilder.buildIntegrationManagerArgs(roomId, integId, screen) + context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) + } + + override fun openRoomWidget(context: Context, roomId: String, widget: Widget) { + val widgetArgs = widgetArgsBuilder.buildRoomWidgetArgs(roomId, widget) + context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) + } + override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) { val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view)) val pairs = ArrayList>() 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 9323a87da5..35ace87b6b 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 @@ -24,6 +24,8 @@ import androidx.fragment.app.Fragment import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.riotx.features.home.room.detail.sticker.StickerPickerConstants import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.settings.VectorSettingsActivity @@ -80,6 +82,15 @@ interface Navigator { token: String?, requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE) + fun openStickerPicker(fragment: Fragment, + roomId: String, + widget: Widget, + requestCode: Int = StickerPickerConstants.STICKER_PICKER_REQUEST_CODE) + + fun openIntegrationManager(context: Context, roomId: String, integId: String?, screen: String?) + + fun openRoomWidget(context: Context, roomId: String, widget: Widget) + fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) diff --git a/vector/src/main/java/im/vector/riotx/features/powerlevel/PowerLevelsObservableFactory.kt b/vector/src/main/java/im/vector/riotx/features/powerlevel/PowerLevelsObservableFactory.kt new file mode 100644 index 0000000000..ee4c20baca --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/powerlevel/PowerLevelsObservableFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.powerlevel + +import im.vector.matrix.android.api.query.QueryStringValue +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.Room +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent +import im.vector.matrix.rx.mapOptional +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.unwrap +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers + +class PowerLevelsObservableFactory(private val room: Room) { + + fun createObservable(): Observable { + return room.rx() + .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + .observeOn(Schedulers.computation()) + .mapOptional { it.content.toModel() } + .unwrap() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipAction.kt b/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipAction.kt new file mode 100644 index 0000000000..b3a1fa6976 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.room + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RequireActiveMembershipAction : VectorViewModelAction { + data class ChangeRoom(val roomId: String) : RequireActiveMembershipAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipViewEvents.kt new file mode 100644 index 0000000000..d3c3e39cfa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipViewEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.room + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class RequireActiveMembershipViewEvents : VectorViewEvents { + data class RoomLeft(val leftMessage: String?) : RequireActiveMembershipViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipViewModel.kt b/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipViewModel.kt new file mode 100644 index 0000000000..e242135218 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipViewModel.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.room + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.jakewharton.rxrelay2.BehaviorRelay +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.Room +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.util.Optional +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.unwrap +import im.vector.riotx.R +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers + +/** + * This ViewModel observe a room summary and notify when the room is left + */ +class RequireActiveMembershipViewModel @AssistedInject constructor( + @Assisted initialState: RequireActiveMembershipViewState, + private val stringProvider: StringProvider, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RequireActiveMembershipViewState): RequireActiveMembershipViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + private val roomIdObservable = BehaviorRelay.createDefault(Optional.from(initialState.roomId)) + + init { + observeRoomSummary() + } + + private fun observeRoomSummary() { + roomIdObservable + .unwrap() + .switchMap { roomId -> + val room = session.getRoom(roomId) ?: return@switchMap Observable.just(Optional.empty()) + room.rx() + .liveRoomSummary() + .unwrap() + .observeOn(Schedulers.computation()) + .map { mapToLeftViewEvent(room, it) } + } + .unwrap() + .subscribe { event -> + _viewEvents.post(event) + } + .disposeOnClear() + } + + private fun mapToLeftViewEvent(room: Room, roomSummary: RoomSummary): Optional { + if (roomSummary.membership.isActive()) { + return Optional.empty() + } + val senderId = room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId))?.senderId + val senderDisplayName = senderId?.takeIf { it != session.myUserId }?.let { + room.getRoomMember(it)?.displayName ?: it + } + val viewEvent = when (roomSummary.membership) { + Membership.LEAVE -> { + val message = senderDisplayName?.let { + stringProvider.getString(R.string.has_been_kicked, roomSummary.displayName, it) + } + RequireActiveMembershipViewEvents.RoomLeft(message) + } + Membership.KNOCK -> { + val message = senderDisplayName?.let { + stringProvider.getString(R.string.has_been_kicked, roomSummary.displayName, it) + } + RequireActiveMembershipViewEvents.RoomLeft(message) + } + Membership.BAN -> { + val message = senderDisplayName?.let { + stringProvider.getString(R.string.has_been_banned, roomSummary.displayName, it) + } + RequireActiveMembershipViewEvents.RoomLeft(message) + } + else -> null + } + return Optional.from(viewEvent) + } + + override fun handle(action: RequireActiveMembershipAction) { + when (action) { + is RequireActiveMembershipAction.ChangeRoom -> { + setState { + copy(roomId = action.roomId) + } + roomIdObservable.accept(Optional.from(action.roomId)) + } + }.exhaustive + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipViewState.kt b/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipViewState.kt new file mode 100644 index 0000000000..ccf0270efc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/room/RequireActiveMembershipViewState.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.room + +import com.airbnb.mvrx.MvRxState +import im.vector.riotx.features.roommemberprofile.RoomMemberProfileArgs +import im.vector.riotx.features.roomprofile.RoomProfileArgs + +data class RequireActiveMembershipViewState( + val roomId: String? = null +) : MvRxState { + + // No constructor for RoomDetailArgs because of intent for Shortcut + + constructor(args: RoomProfileArgs) : this(roomId = args.roomId) + + constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId) +} diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt index 1dc2459538..e60f268b22 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt @@ -22,6 +22,10 @@ import im.vector.riotx.core.platform.VectorViewModelAction sealed class RoomMemberProfileAction : VectorViewModelAction { object RetryFetchingInfo : RoomMemberProfileAction() object IgnoreUser : RoomMemberProfileAction() + data class BanOrUnbanUser(val reason: String?) : RoomMemberProfileAction() + data class KickUser(val reason: String?) : RoomMemberProfileAction() + object InviteUser : RoomMemberProfileAction() object VerifyUser : RoomMemberProfileAction() object ShareRoomMemberProfile : RoomMemberProfileAction() + data class SetPowerLevel(val previousValue: Int, val newValue: Int, val askForValidation: Boolean) : RoomMemberProfileAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileActivity.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileActivity.kt index 25efecf541..3323c29fee 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileActivity.kt @@ -19,36 +19,70 @@ package im.vector.riotx.features.roommemberprofile import android.content.Context import android.content.Intent +import android.widget.Toast import androidx.appcompat.widget.Toolbar +import com.airbnb.mvrx.MvRx +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.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.room.RequireActiveMembershipViewEvents +import im.vector.riotx.features.room.RequireActiveMembershipViewModel +import im.vector.riotx.features.room.RequireActiveMembershipViewState +import javax.inject.Inject -class RoomMemberProfileActivity : VectorBaseActivity(), ToolbarConfigurable { +class RoomMemberProfileActivity : + VectorBaseActivity(), + ToolbarConfigurable, + RequireActiveMembershipViewModel.Factory { companion object { - - private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" - fun newIntent(context: Context, args: RoomMemberProfileArgs): Intent { return Intent(context, RoomMemberProfileActivity::class.java).apply { - putExtra(EXTRA_FRAGMENT_ARGS, args) + putExtra(MvRx.KEY_ARG, args) } } } + private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel() + + @Inject + lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory + + override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel { + return requireActiveMembershipViewModelFactory.create(initialState) + } + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + override fun getLayoutRes() = R.layout.activity_simple override fun initUiAndData() { if (isFirstCreation()) { - val fragmentArgs: RoomMemberProfileArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) - ?: return + val fragmentArgs: RoomMemberProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return addFragment(R.id.simpleFragmentContainer, RoomMemberProfileFragment::class.java, fragmentArgs) } + + requireActiveMembershipViewModel.observeViewEvents { + when (it) { + is RequireActiveMembershipViewEvents.RoomLeft -> handleRoomLeft(it) + } + } } override fun configure(toolbar: Toolbar) { configureToolbar(toolbar) } + + private fun handleRoomLeft(roomLeft: RequireActiveMembershipViewEvents.RoomLeft) { + if (roomLeft.leftMessage != null) { + Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show() + } + finish() + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt index 70c3e8add2..1ad72ad424 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt @@ -18,6 +18,10 @@ package im.vector.riotx.features.roommemberprofile import com.airbnb.epoxy.TypedEpoxyController +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.powerlevels.PowerLevelsHelper +import im.vector.matrix.android.api.session.room.powerlevels.Role import im.vector.riotx.R import im.vector.riotx.core.epoxy.profiles.buildProfileAction import im.vector.riotx.core.epoxy.profiles.buildProfileSection @@ -28,7 +32,8 @@ import javax.inject.Inject class RoomMemberProfileController @Inject constructor( private val stringProvider: StringProvider, - colorProvider: ColorProvider + colorProvider: ColorProvider, + private val session: Session ) : TypedEpoxyController() { private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) @@ -42,6 +47,11 @@ class RoomMemberProfileController @Inject constructor( fun onShowDeviceListNoCrossSigning() fun onJumpToReadReceiptClicked() fun onMentionClicked() + fun onEditPowerLevel(currentRole: Role) + fun onKickClicked() + fun onBanClicked(isUserBanned: Boolean) + fun onCancelInviteClicked() + fun onInviteClicked() } override fun buildModels(data: RoomMemberProfileViewState?) { @@ -71,6 +81,12 @@ class RoomMemberProfileController @Inject constructor( } private fun buildRoomMemberActions(state: RoomMemberProfileViewState) { + buildSecuritySection(state) + buildMoreSection(state) + buildAdminSection(state) + } + + private fun buildSecuritySection(state: RoomMemberProfileViewState) { // Security buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) @@ -148,9 +164,13 @@ class RoomMemberProfileController @Inject constructor( centered(false) } } + } + private fun buildMoreSection(state: RoomMemberProfileViewState) { // More if (!state.isMine) { + val membership = state.asyncMembership() ?: return + buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) buildProfileAction( id = "read_receipt", @@ -170,6 +190,19 @@ class RoomMemberProfileController @Inject constructor( divider = ignoreActionTitle != null, action = { callback?.onMentionClicked() } ) + + val canInvite = state.actionPermissions.canInvite + if (canInvite && (membership == Membership.LEAVE || membership == Membership.KNOCK)) { + buildProfileAction( + id = "invite", + title = stringProvider.getString(R.string.room_participants_action_invite), + dividerColor = dividerColor, + destructive = false, + editable = false, + divider = ignoreActionTitle != null, + action = { callback?.onInviteClicked() } + ) + } if (ignoreActionTitle != null) { buildProfileAction( id = "ignore", @@ -184,6 +217,77 @@ class RoomMemberProfileController @Inject constructor( } } + private fun buildAdminSection(state: RoomMemberProfileViewState) { + val powerLevelsContent = state.powerLevelsContent ?: return + val powerLevelsStr = state.userPowerLevelString() ?: return + val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + val userPowerLevel = powerLevelsHelper.getUserRole(state.userId) + val myPowerLevel = powerLevelsHelper.getUserRole(session.myUserId) + if ((!state.isMine && myPowerLevel <= userPowerLevel)) { + return + } + val membership = state.asyncMembership() ?: return + val canKick = !state.isMine && state.actionPermissions.canKick + val canBan = !state.isMine && state.actionPermissions.canBan + val canEditPowerLevel = state.actionPermissions.canEditPowerLevel + if (canKick || canBan || canEditPowerLevel) { + buildProfileSection(stringProvider.getString(R.string.room_profile_section_admin)) + } + if (canEditPowerLevel) { + buildProfileAction( + id = "edit_power_level", + editable = false, + title = powerLevelsStr, + divider = canKick || canBan, + dividerColor = dividerColor, + action = { callback?.onEditPowerLevel(userPowerLevel) } + ) + } + + if (canKick) { + when (membership) { + Membership.JOIN -> { + buildProfileAction( + id = "kick", + editable = false, + divider = canBan, + destructive = true, + title = stringProvider.getString(R.string.room_participants_action_kick), + dividerColor = dividerColor, + action = { callback?.onKickClicked() } + ) + } + Membership.INVITE -> { + buildProfileAction( + id = "cancel_invite", + title = stringProvider.getString(R.string.room_participants_action_cancel_invite), + divider = canBan, + dividerColor = dividerColor, + destructive = true, + editable = false, + action = { callback?.onCancelInviteClicked() } + ) + } + else -> Unit + } + } + if (canBan) { + val banActionTitle = if (membership == Membership.BAN) { + stringProvider.getString(R.string.room_participants_action_unban) + } else { + stringProvider.getString(R.string.room_participants_action_ban) + } + buildProfileAction( + id = "ban", + editable = false, + destructive = true, + title = banActionTitle, + dividerColor = dividerColor, + action = { callback?.onBanClicked(membership == Membership.BAN) } + ) + } + } + private fun RoomMemberProfileViewState.buildIgnoreActionTitle(): String? { val isIgnored = isIgnored() ?: return null return if (isIgnored) { diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt index 28734af0ad..e9f4a736ef 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -29,10 +29,12 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.room.powerlevels.Role import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.animations.AppBarStateChangeListener import im.vector.riotx.core.animations.MatrixItemAppBarStateChangeListener +import im.vector.riotx.core.dialogs.ConfirmationDialogBuilder import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.exhaustive @@ -43,6 +45,7 @@ import im.vector.riotx.core.utils.startSharePlainTextIntent import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.roommemberprofile.devices.DeviceListBottomSheet +import im.vector.riotx.features.roommemberprofile.powerlevel.EditPowerLevelDialogs import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_matrix_profile.* import kotlinx.android.synthetic.main.view_stub_room_member_profile_header.* @@ -63,7 +66,7 @@ class RoomMemberProfileFragment @Inject constructor( private val fragmentArgs: RoomMemberProfileArgs by args() private val viewModel: RoomMemberProfileViewModel by fragmentViewModel() - private lateinit var appBarStateChangeListener: AppBarStateChangeListener + private var appBarStateChangeListener: AppBarStateChangeListener? = null override fun getLayoutResId() = R.layout.fragment_matrix_profile @@ -82,7 +85,7 @@ class RoomMemberProfileFragment @Inject constructor( } } memberProfileStateView.contentView = memberProfileInfoContainer - matrixProfileRecyclerView.configureWith(roomMemberProfileController, hasFixedSize = true) + matrixProfileRecyclerView.configureWith(roomMemberProfileController, hasFixedSize = true, disableItemAnimation = true) roomMemberProfileController.callback = this appBarStateChangeListener = MatrixItemAppBarStateChangeListener(headerView, listOf( @@ -94,15 +97,33 @@ class RoomMemberProfileFragment @Inject constructor( matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) viewModel.observeViewEvents { when (it) { - is RoomMemberProfileViewEvents.Loading -> showLoading(it.message) - is RoomMemberProfileViewEvents.Failure -> showFailure(it.throwable) - is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit - is RoomMemberProfileViewEvents.StartVerification -> handleStartVerification(it) - is RoomMemberProfileViewEvents.ShareRoomMemberProfile -> handleShareRoomMemberProfile(it.permalink) + is RoomMemberProfileViewEvents.Loading -> showLoading(it.message) + is RoomMemberProfileViewEvents.Failure -> showFailure(it.throwable) + is RoomMemberProfileViewEvents.StartVerification -> handleStartVerification(it) + is RoomMemberProfileViewEvents.ShareRoomMemberProfile -> handleShareRoomMemberProfile(it.permalink) + is RoomMemberProfileViewEvents.ShowPowerLevelValidation -> handleShowPowerLevelAdminWarning(it) + is RoomMemberProfileViewEvents.ShowPowerLevelDemoteWarning -> handleShowPowerLevelDemoteWarning(it) + is RoomMemberProfileViewEvents.OnKickActionSuccess -> Unit + is RoomMemberProfileViewEvents.OnSetPowerLevelSuccess -> Unit + is RoomMemberProfileViewEvents.OnBanActionSuccess -> Unit + is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit + is RoomMemberProfileViewEvents.OnInviteActionSuccess -> Unit }.exhaustive } } + private fun handleShowPowerLevelDemoteWarning(event: RoomMemberProfileViewEvents.ShowPowerLevelDemoteWarning) { + EditPowerLevelDialogs.showDemoteWarning(requireActivity()) { + viewModel.handle(RoomMemberProfileAction.SetPowerLevel(event.currentValue, event.newValue, false)) + } + } + + private fun handleShowPowerLevelAdminWarning(event: RoomMemberProfileViewEvents.ShowPowerLevelValidation) { + EditPowerLevelDialogs.showValidation(requireActivity()) { + viewModel.handle(RoomMemberProfileAction.SetPowerLevel(event.currentValue, event.newValue, false)) + } + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.roomMemberProfileShareAction -> { @@ -135,6 +156,7 @@ class RoomMemberProfileFragment @Inject constructor( override fun onDestroyView() { matrixProfileAppBarLayout.removeOnOffsetChangedListener(appBarStateChangeListener) roomMemberProfileController.callback = null + appBarStateChangeListener = null matrixProfileRecyclerView.cleanup() super.onDestroyView() } @@ -207,8 +229,31 @@ class RoomMemberProfileFragment @Inject constructor( // RoomMemberProfileController.Callback - override fun onIgnoreClicked() { - viewModel.handle(RoomMemberProfileAction.IgnoreUser) + override fun onIgnoreClicked() = withState(viewModel) { state -> + val isIgnored = state.isIgnored() ?: false + val titleRes: Int + val positiveButtonRes: Int + val confirmationRes: Int + if (isIgnored) { + confirmationRes = R.string.room_participants_action_unignore_prompt_msg + titleRes = R.string.room_participants_action_unignore_title + positiveButtonRes = R.string.room_participants_action_unignore + } else { + confirmationRes = R.string.room_participants_action_ignore_prompt_msg + titleRes = R.string.room_participants_action_ignore_title + positiveButtonRes = R.string.room_participants_action_ignore + } + ConfirmationDialogBuilder + .show( + activity = requireActivity(), + askForReason = false, + confirmationRes = confirmationRes, + positiveRes = positiveButtonRes, + reasonHintRes = 0, + titleRes = titleRes + ) { + viewModel.handle(RoomMemberProfileAction.IgnoreUser) + } } override fun onTapVerify() { @@ -238,4 +283,68 @@ class RoomMemberProfileFragment @Inject constructor( private fun onAvatarClicked(view: View, userMatrixItem: MatrixItem) { navigator.openBigImageViewer(requireActivity(), view, userMatrixItem) } + + override fun onEditPowerLevel(currentRole: Role) { + EditPowerLevelDialogs.showChoice(requireActivity(), currentRole) { newPowerLevel -> + viewModel.handle(RoomMemberProfileAction.SetPowerLevel(currentRole.value, newPowerLevel, true)) + } + } + + override fun onKickClicked() { + ConfirmationDialogBuilder + .show( + activity = requireActivity(), + askForReason = true, + confirmationRes = R.string.room_participants_kick_prompt_msg, + positiveRes = R.string.room_participants_action_kick, + reasonHintRes = R.string.room_participants_kick_reason, + titleRes = R.string.room_participants_kick_title + ) { reason -> + viewModel.handle(RoomMemberProfileAction.KickUser(reason)) + } + } + + override fun onBanClicked(isUserBanned: Boolean) { + val titleRes: Int + val positiveButtonRes: Int + val confirmationRes: Int + if (isUserBanned) { + confirmationRes = R.string.room_participants_unban_prompt_msg + titleRes = R.string.room_participants_unban_title + positiveButtonRes = R.string.room_participants_action_unban + } else { + confirmationRes = R.string.room_participants_ban_prompt_msg + titleRes = R.string.room_participants_ban_title + positiveButtonRes = R.string.room_participants_action_ban + } + ConfirmationDialogBuilder + .show( + activity = requireActivity(), + askForReason = !isUserBanned, + confirmationRes = confirmationRes, + positiveRes = positiveButtonRes, + reasonHintRes = R.string.room_participants_ban_reason, + titleRes = titleRes + ) { reason -> + viewModel.handle(RoomMemberProfileAction.BanOrUnbanUser(reason)) + } + } + + override fun onCancelInviteClicked() { + ConfirmationDialogBuilder + .show( + activity = requireActivity(), + askForReason = false, + confirmationRes = R.string.room_participants_action_cancel_invite_prompt_msg, + positiveRes = R.string.room_participants_action_cancel_invite, + reasonHintRes = 0, + titleRes = R.string.room_participants_action_cancel_invite_title + ) { + viewModel.handle(RoomMemberProfileAction.KickUser(null)) + } + } + + override fun onInviteClicked() { + viewModel.handle(RoomMemberProfileAction.InviteUser) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt index 5d8757a337..69c73007e0 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt @@ -26,6 +26,12 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents() object OnIgnoreActionSuccess : RoomMemberProfileViewEvents() + object OnSetPowerLevelSuccess : RoomMemberProfileViewEvents() + object OnInviteActionSuccess : RoomMemberProfileViewEvents() + object OnKickActionSuccess : RoomMemberProfileViewEvents() + object OnBanActionSuccess : RoomMemberProfileViewEvents() + data class ShowPowerLevelValidation(val currentValue: Int, val newValue: Int) : RoomMemberProfileViewEvents() + data class ShowPowerLevelDemoteWarning(val currentValue: Int, val newValue: Int) : RoomMemberProfileViewEvents() data class StartVerification( val userId: String, diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt index 44c214bc99..aca584a663 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -18,7 +18,9 @@ package im.vector.riotx.features.roommemberprofile import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized @@ -30,23 +32,25 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.PowerLevelsContent import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper +import im.vector.matrix.android.api.session.room.powerlevels.Role import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toOptional -import im.vector.matrix.rx.mapOptional +import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.R import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory import io.reactivex.Observable import io.reactivex.functions.BiFunction import kotlinx.coroutines.Dispatchers @@ -140,6 +144,36 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v is RoomMemberProfileAction.IgnoreUser -> handleIgnoreAction() is RoomMemberProfileAction.VerifyUser -> prepareVerification() is RoomMemberProfileAction.ShareRoomMemberProfile -> handleShareRoomMemberProfile() + is RoomMemberProfileAction.SetPowerLevel -> handleSetPowerLevel(action) + is RoomMemberProfileAction.BanOrUnbanUser -> handleBanOrUnbanAction(action) + is RoomMemberProfileAction.KickUser -> handleKickAction(action) + RoomMemberProfileAction.InviteUser -> handleInviteAction() + } + } + + private fun handleSetPowerLevel(action: RoomMemberProfileAction.SetPowerLevel) = withState { state -> + if (room == null || action.previousValue == action.newValue) { + return@withState + } + val currentPowerLevelsContent = state.powerLevelsContent ?: return@withState + val myPowerLevel = PowerLevelsHelper(currentPowerLevelsContent).getUserPowerLevelValue(session.myUserId) + if (action.askForValidation && action.newValue >= myPowerLevel) { + _viewEvents.post(RoomMemberProfileViewEvents.ShowPowerLevelValidation(action.previousValue, action.newValue)) + } else if (action.askForValidation && state.isMine) { + _viewEvents.post(RoomMemberProfileViewEvents.ShowPowerLevelDemoteWarning(action.previousValue, action.newValue)) + } else { + currentPowerLevelsContent.users[state.userId] = action.newValue + viewModelScope.launch { + _viewEvents.post(RoomMemberProfileViewEvents.Loading()) + try { + awaitCallback { + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent(), it) + } + _viewEvents.post(RoomMemberProfileViewEvents.OnSetPowerLevelSuccess) + } catch (failure: Throwable) { + _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) + } + } } } @@ -156,15 +190,79 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v } } + private fun handleInviteAction() { + if (room == null) { + return + } + viewModelScope.launch { + try { + _viewEvents.post(RoomMemberProfileViewEvents.Loading()) + awaitCallback { + room.invite(initialState.userId, callback = it) + } + _viewEvents.post(RoomMemberProfileViewEvents.OnInviteActionSuccess) + } catch (failure: Throwable) { + _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) + } + } + } + + private fun handleKickAction(action: RoomMemberProfileAction.KickUser) { + if (room == null) { + return + } + viewModelScope.launch { + try { + _viewEvents.post(RoomMemberProfileViewEvents.Loading()) + awaitCallback { + room.kick(initialState.userId, action.reason, it) + } + _viewEvents.post(RoomMemberProfileViewEvents.OnKickActionSuccess) + } catch (failure: Throwable) { + _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) + } + } + } + + private fun handleBanOrUnbanAction(action: RoomMemberProfileAction.BanOrUnbanUser) = withState { state -> + if (room == null) { + return@withState + } + val membership = state.asyncMembership() ?: return@withState + viewModelScope.launch { + try { + _viewEvents.post(RoomMemberProfileViewEvents.Loading()) + awaitCallback { + if (membership == Membership.BAN) { + room.unban(initialState.userId, action.reason, it) + } else { + room.ban(initialState.userId, action.reason, it) + } + } + _viewEvents.post(RoomMemberProfileViewEvents.OnBanActionSuccess) + } catch (failure: Throwable) { + _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) + } + } + } + private fun observeRoomMemberSummary(room: Room) { val queryParams = roomMemberQueryParams { this.userId = QueryStringValue.Equals(initialState.userId, QueryStringValue.Case.SENSITIVE) } room.rx().liveRoomMembers(queryParams) - .map { it.firstOrNull()?.toMatrixItem().toOptional() } + .map { it.firstOrNull().toOptional() } .unwrap() .execute { - copy(userMatrixItem = it) + when (it) { + is Loading -> copy(userMatrixItem = Loading(), asyncMembership = Loading()) + is Success -> copy( + userMatrixItem = Success(it().toMatrixItem()), + asyncMembership = Success(it().membership) + ) + is Fail -> copy(userMatrixItem = Fail(it.error), asyncMembership = Fail(it.error)) + is Uninitialized -> this + } } } @@ -184,17 +282,22 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v private fun observeRoomSummaryAndPowerLevels(room: Room) { val roomSummaryLive = room.rx().liveRoomSummary().unwrap() - val powerLevelsContentLive = room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "") - .mapOptional { it.content.toModel() } - .unwrap() + val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable() + + powerLevelsContentLive.subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + val permissions = ActionPermissions( + canKick = powerLevelsHelper.isUserAbleToKick(session.myUserId), + canBan = powerLevelsHelper.isUserAbleToBan(session.myUserId), + canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId), + canEditPowerLevel = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_POWER_LEVELS) + ) + setState { copy(powerLevelsContent = it, actionPermissions = permissions) } + }.disposeOnClear() roomSummaryLive.execute { copy(isRoomEncrypted = it.invoke()?.isEncrypted == true) } - powerLevelsContentLive.execute { - copy(powerLevelsContent = it) - } - Observable .combineLatest( roomSummaryLive, @@ -202,15 +305,11 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v BiFunction { roomSummary, powerLevelsContent -> val roomName = roomSummary.toMatrixItem().getBestName() val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) - val userPowerLevel = powerLevelsHelper.getUserPowerLevel(initialState.userId) - if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_ADMIN_LEVEL) { - stringProvider.getString(R.string.room_member_power_level_admin_in, roomName) - } else if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL) { - stringProvider.getString(R.string.room_member_power_level_moderator_in, roomName) - } else if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL) { - "" - } else { - stringProvider.getString(R.string.room_member_power_level_custom_in, userPowerLevel, roomName) + when (val userPowerLevel = powerLevelsHelper.getUserRole(initialState.userId)) { + Role.Admin -> stringProvider.getString(R.string.room_member_power_level_admin_in, roomName) + Role.Moderator -> stringProvider.getString(R.string.room_member_power_level_moderator_in, roomName) + Role.Default -> stringProvider.getString(R.string.room_member_power_level_default_in, roomName) + is Role.Custom -> stringProvider.getString(R.string.room_member_power_level_custom_in, userPowerLevel.value, roomName) } } ).execute { diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt index a5c140d0ab..81423d2b9e 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.PowerLevelsContent import im.vector.matrix.android.api.util.MatrixItem @@ -31,13 +32,22 @@ data class RoomMemberProfileViewState( val isMine: Boolean = false, val isIgnored: Async = Uninitialized, val isRoomEncrypted: Boolean = false, - val powerLevelsContent: Async = Uninitialized, + val powerLevelsContent: PowerLevelsContent? = null, val userPowerLevelString: Async = Uninitialized, val userMatrixItem: Async = Uninitialized, val userMXCrossSigningInfo: MXCrossSigningInfo? = null, val allDevicesAreTrusted: Boolean = false, - val allDevicesAreCrossSignedTrusted: Boolean = false + val allDevicesAreCrossSignedTrusted: Boolean = false, + val asyncMembership: Async = Uninitialized, + val actionPermissions: ActionPermissions = ActionPermissions() ) : MvRxState { - constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId) + constructor(args: RoomMemberProfileArgs) : this(userId = args.userId, roomId = args.roomId) } + +data class ActionPermissions( + val canKick: Boolean = false, + val canBan: Boolean = false, + val canInvite: Boolean = false, + val canEditPowerLevel: Boolean = false +) diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListFragment.kt index 93c51b2008..5fdb776b12 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListFragment.kt @@ -58,7 +58,6 @@ class DeviceListFragment @Inject constructor( override fun invalidate() = withState(viewModel) { epoxyController.setData(it) - super.invalidate() } override fun onDeviceSelected(device: CryptoDeviceInfo) { diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt index d955e4c9dc..2e943b0eff 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt @@ -58,7 +58,6 @@ class DeviceTrustInfoActionFragment @Inject constructor( override fun invalidate() = withState(viewModel) { epoxyController.setData(it) - super.invalidate() } override fun onVerifyManually(device: CryptoDeviceInfo) { diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt new file mode 100644 index 0000000000..5672c621c7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roommemberprofile.powerlevel + +import android.app.Activity +import android.content.DialogInterface +import android.view.KeyEvent +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import im.vector.matrix.android.api.session.room.powerlevels.Role +import im.vector.riotx.R +import im.vector.riotx.core.extensions.hideKeyboard +import kotlinx.android.synthetic.main.dialog_edit_power_level.view.* + +object EditPowerLevelDialogs { + + fun showChoice(activity: Activity, currentRole: Role, listener: (Int) -> Unit) { + val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_edit_power_level, null) + dialogLayout.powerLevelRadioGroup.setOnCheckedChangeListener { _, checkedId -> + dialogLayout.powerLevelCustomEditLayout.isVisible = checkedId == R.id.powerLevelCustomRadio + } + dialogLayout.powerLevelCustomEdit.setText(currentRole.value.toString()) + + when (currentRole) { + Role.Admin -> dialogLayout.powerLevelAdminRadio.isChecked = true + Role.Moderator -> dialogLayout.powerLevelModeratorRadio.isChecked = true + Role.Default -> dialogLayout.powerLevelDefaultRadio.isChecked = true + else -> dialogLayout.powerLevelCustomRadio.isChecked = true + } + + AlertDialog.Builder(activity) + .setTitle(R.string.power_level_edit_title) + .setView(dialogLayout) + .setPositiveButton(R.string.edit) { _, _ -> + val newValue = when (dialogLayout.powerLevelRadioGroup.checkedRadioButtonId) { + R.id.powerLevelAdminRadio -> Role.Admin.value + R.id.powerLevelModeratorRadio -> Role.Moderator.value + R.id.powerLevelDefaultRadio -> Role.Default.value + else -> { + dialogLayout.powerLevelCustomEdit.text?.toString()?.toInt() ?: currentRole.value + } + } + listener(newValue) + } + .setNegativeButton(R.string.cancel, null) + .setOnKeyListener(DialogInterface.OnKeyListener + { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + return@OnKeyListener true + } + false + }) + .setOnDismissListener { + dialogLayout.hideKeyboard() + } + .create() + .show() + } + + fun showValidation(activity: Activity, onValidate: () -> Unit) { + // Ask to the user the confirmation to upgrade. + AlertDialog.Builder(activity) + .setMessage(R.string.room_participants_power_level_prompt) + .setPositiveButton(R.string.yes) { _, _ -> + onValidate() + } + .setNegativeButton(R.string.no, null) + .show() + } + + fun showDemoteWarning(activity: Activity, onValidate: () -> Unit) { + // Ask to the user the confirmation to downgrade his own role. + AlertDialog.Builder(activity) + .setTitle(R.string.room_participants_power_level_demote_warning_title) + .setMessage(R.string.room_participants_power_level_demote_warning_prompt) + .setPositiveButton(R.string.room_participants_power_level_demote) { _, _ -> + onValidate() + } + .setNegativeButton(R.string.cancel, null) + .show() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt index bfc815f1ed..b61075be80 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt @@ -19,26 +19,35 @@ package im.vector.riotx.features.roomprofile import android.content.Context import android.content.Intent +import android.widget.Toast import androidx.appcompat.widget.Toolbar +import com.airbnb.mvrx.MvRx +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.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.room.RequireActiveMembershipViewEvents +import im.vector.riotx.features.room.RequireActiveMembershipViewModel +import im.vector.riotx.features.room.RequireActiveMembershipViewState import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment +import javax.inject.Inject -class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable { +class RoomProfileActivity : + VectorBaseActivity(), + ToolbarConfigurable, + RequireActiveMembershipViewModel.Factory { companion object { - private const val EXTRA_ROOM_PROFILE_ARGS = "EXTRA_ROOM_PROFILE_ARGS" - fun newIntent(context: Context, roomId: String): Intent { val roomProfileArgs = RoomProfileArgs(roomId) return Intent(context, RoomProfileActivity::class.java).apply { - putExtra(EXTRA_ROOM_PROFILE_ARGS, roomProfileArgs) + putExtra(MvRx.KEY_ARG, roomProfileArgs) } } } @@ -46,11 +55,25 @@ class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable { private lateinit var sharedActionViewModel: RoomProfileSharedActionViewModel private lateinit var roomProfileArgs: RoomProfileArgs + private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel() + + @Inject + lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory + + override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel { + return requireActiveMembershipViewModelFactory.create(initialState) + } + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + override fun getLayoutRes() = R.layout.activity_simple override fun initUiAndData() { sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java) - roomProfileArgs = intent?.extras?.getParcelable(EXTRA_ROOM_PROFILE_ARGS) ?: return + roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return if (isFirstCreation()) { addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs) } @@ -64,6 +87,19 @@ class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable { } } .disposeOnDestroy() + + requireActiveMembershipViewModel.observeViewEvents { + when (it) { + is RequireActiveMembershipViewEvents.RoomLeft -> handleRoomLeft(it) + } + } + } + + private fun handleRoomLeft(roomLeft: RequireActiveMembershipViewEvents.RoomLeft) { + if (roomLeft.leftMessage != null) { + Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show() + } + finish() } private fun openRoomUploads() { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt index 58c8fead32..52f2d95c93 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -67,7 +67,7 @@ class RoomProfileFragment @Inject constructor( private lateinit var roomProfileSharedActionViewModel: RoomProfileSharedActionViewModel private val roomProfileViewModel: RoomProfileViewModel by fragmentViewModel() - private lateinit var appBarStateChangeListener: AppBarStateChangeListener + private var appBarStateChangeListener: AppBarStateChangeListener? = null override fun getLayoutResId() = R.layout.fragment_matrix_profile @@ -140,13 +140,14 @@ class RoomProfileFragment @Inject constructor( private fun setupRecyclerView() { roomProfileController.callback = this - matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true) + matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true, disableItemAnimation = true) } override fun onDestroyView() { super.onDestroyView() matrixProfileAppBarLayout.removeOnOffsetChangedListener(appBarStateChangeListener) matrixProfileRecyclerView.cleanup() + appBarStateChangeListener = null } override fun invalidate() = withState(roomProfileViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt index 8a08cbae8a..6bd2b5d0e3 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.roomprofile.members import android.os.Bundle +import android.view.Menu import android.view.MenuItem import android.view.View import com.airbnb.mvrx.args @@ -46,6 +47,13 @@ class RoomMemberListFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_room_member_list + override fun onPrepareOptionsMenu(menu: Menu) { + val canInvite = withState(viewModel) { + it.actionsPermissions.canInvite + } + menu.findItem(R.id.menu_room_member_list_add_member).isVisible = canInvite + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_room_member_list_add_member -> { @@ -61,6 +69,9 @@ class RoomMemberListFragment @Inject constructor( roomMemberListController.callback = this setupToolbar(roomSettingsToolbar) recyclerView.configureWith(roomMemberListController, hasFixedSize = true) + viewModel.selectSubscribe(this, RoomMemberListViewState::actionsPermissions) { + invalidateOptionsMenu() + } } override fun onDestroyView() { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt index 81b2809c4f..f177d26725 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt @@ -31,14 +31,15 @@ import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.PowerLevelsContent import im.vector.matrix.android.api.session.room.model.RoomMemberSummary -import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper +import im.vector.matrix.android.api.session.room.powerlevels.Role import im.vector.matrix.rx.asObservable import im.vector.matrix.rx.mapOptional import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.functions.BiFunction @@ -68,6 +69,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState init { observeRoomMemberSummaries() observeRoomSummary() + observePowerLevel() } private fun observeRoomMemberSummaries() { @@ -80,7 +82,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState .combineLatest, PowerLevelsContent, RoomMemberSummaries>( room.rx().liveRoomMembers(roomMemberQueryParams), room.rx() - .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "") + .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) .mapOptional { it.content.toModel() } .unwrap(), BiFunction { roomMembers, powerLevelsContent -> @@ -118,6 +120,18 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState } } + private fun observePowerLevel() { + PowerLevelsObservableFactory(room).createObservable() + .subscribe { + val permissions = ActionPermissions( + canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId) + ) + setState { + copy(actionsPermissions = permissions) + } + }.disposeOnClear() + } + private fun observeRoomSummary() { room.rx().liveRoomSummary() .unwrap() @@ -135,22 +149,22 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) roomMembers .forEach { roomMember -> - val memberPowerLevel = powerLevelsHelper.getUserPowerLevel(roomMember.userId) + val userRole = powerLevelsHelper.getUserRole(roomMember.userId) when { - roomMember.membership == Membership.INVITE -> invites.add(roomMember) - memberPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_ADMIN_LEVEL -> admins.add(roomMember) - memberPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL -> moderators.add(roomMember) - memberPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL -> users.add(roomMember) - else -> customs.add(roomMember) + roomMember.membership == Membership.INVITE -> invites.add(roomMember) + userRole == Role.Admin -> admins.add(roomMember) + userRole == Role.Moderator -> moderators.add(roomMember) + userRole == Role.Default -> users.add(roomMember) + else -> customs.add(roomMember) } } return listOf( - PowerLevelCategory.ADMIN to admins.sortedWith(roomMemberSummaryComparator), - PowerLevelCategory.MODERATOR to moderators.sortedWith(roomMemberSummaryComparator), - PowerLevelCategory.CUSTOM to customs.sortedWith(roomMemberSummaryComparator), - PowerLevelCategory.INVITE to invites.sortedWith(roomMemberSummaryComparator), - PowerLevelCategory.USER to users.sortedWith(roomMemberSummaryComparator) + RoomMemberListCategories.ADMIN to admins.sortedWith(roomMemberSummaryComparator), + RoomMemberListCategories.MODERATOR to moderators.sortedWith(roomMemberSummaryComparator), + RoomMemberListCategories.CUSTOM to customs.sortedWith(roomMemberSummaryComparator), + RoomMemberListCategories.INVITE to invites.sortedWith(roomMemberSummaryComparator), + RoomMemberListCategories.USER to users.sortedWith(roomMemberSummaryComparator) ) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt index 4618a07cb0..ece49a178c 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt @@ -30,15 +30,20 @@ data class RoomMemberListViewState( val roomId: String, val roomSummary: Async = Uninitialized, val roomMemberSummaries: Async = Uninitialized, - val trustLevelMap: Async> = Uninitialized + val trustLevelMap: Async> = Uninitialized, + val actionsPermissions: ActionPermissions = ActionPermissions() ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) } -typealias RoomMemberSummaries = List>> +data class ActionPermissions( + val canInvite: Boolean = false +) -enum class PowerLevelCategory(@StringRes val titleRes: Int) { +typealias RoomMemberSummaries = List>> + +enum class RoomMemberListCategories(@StringRes val titleRes: Int) { ADMIN(R.string.room_member_power_level_admins), MODERATOR(R.string.room_member_power_level_moderators), CUSTOM(R.string.room_member_power_level_custom), 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 fcc24d35a7..685f1d6491 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 @@ -69,7 +69,8 @@ class VectorPreferences @Inject constructor(private val context: Context) { const val SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY" const val SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY" const val SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY" - + const val SETTINGS_ALLOW_INTEGRATIONS_KEY = "SETTINGS_ALLOW_INTEGRATIONS_KEY" + const val SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY = "SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY" const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY" // user 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 dfa88d9b87..926d285f7b 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 @@ -31,18 +31,23 @@ import androidx.core.view.isVisible import androidx.preference.EditTextPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory +import androidx.preference.SwitchPreference import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.cache.DiskCache import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.failure.isInvalidPassword +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService import im.vector.riotx.R import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.core.preference.VectorPreference +import im.vector.riotx.core.preference.VectorSwitchPreference import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA import im.vector.riotx.core.utils.TextUtils import im.vector.riotx.core.utils.allGranted @@ -92,6 +97,16 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { findPreference(VectorPreferences.SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY)!! } + private val integrationServiceListener = object : IntegrationManagerService.Listener { + override fun onConfigurationChanged(configs: List) { + refreshIntegrationManagerSettings() + } + + override fun onIsEnabledChanged(enabled: Boolean) { + refreshIntegrationManagerSettings() + } + } + override fun bindPref() { // Avatar mUserAvatarPreference.let { @@ -196,6 +211,15 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { } } + (findPreference(VectorPreferences.SETTINGS_ALLOW_INTEGRATIONS_KEY) as? VectorSwitchPreference)?.let { + it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + // Disable it while updating the state, will be re-enabled by the account data listener. + it.isEnabled = false + session.integrationManagerService().setIntegrationEnabled(newValue as Boolean, NoOpMatrixCallback()) + true + } + } + // clear medias cache findPreference(VectorPreferences.SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY)!!.let { val size = getSizeOfFiles(File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR)) @@ -241,9 +265,15 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { override fun onResume() { super.onResume() - // Refresh identity server summary mIdentityServerPreference.summary = session.identityService().getCurrentIdentityServerUrl() ?: getString(R.string.identity_server_not_defined) + refreshIntegrationManagerSettings() + session.integrationManagerService().addListener(integrationServiceListener) + } + + override fun onPause() { + super.onPause() + session.integrationManagerService().removeListener(integrationServiceListener) } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { @@ -315,6 +345,25 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { } } + private fun refreshIntegrationManagerSettings() { + val integrationAllowed = session.integrationManagerService().isIntegrationEnabled() + (findPreference(VectorPreferences.SETTINGS_ALLOW_INTEGRATIONS_KEY))!!.let { + val savedListener = it.onPreferenceChangeListener + it.onPreferenceChangeListener = null + it.isChecked = integrationAllowed + it.isEnabled = true + it.onPreferenceChangeListener = savedListener + } + findPreference(VectorPreferences.SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY)!!.let { + if (integrationAllowed) { + it.summary = session.integrationManagerService().getPreferredConfig().uiUrl + it.isVisible = true + } else { + it.isVisible = false + } + } + } + /** * Update the avatar. */ 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 f497e91b47..95ed9cd661 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 @@ -55,7 +55,7 @@ class VectorSettingsPreferencesFragment @Inject constructor( findPreference(ThemeUtils.APPLICATION_THEME_KEY)!! .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue is String) { - ThemeUtils.setApplicationLightTheme(requireContext(), newValue) + ThemeUtils.setApplicationLightTheme(requireContext().applicationContext, newValue) // Restart the Activity activity?.restart() true @@ -66,7 +66,7 @@ class VectorSettingsPreferencesFragment @Inject constructor( findPreference(ThemeUtils.APPLICATION_DARK_THEME_KEY)!! .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue is String) { - ThemeUtils.setApplicationDarkTheme(requireContext(), newValue) + ThemeUtils.setApplicationDarkTheme(requireContext().applicationContext, newValue) // Restart the Activity activity?.let { // Note: recreate does not apply the color correctly diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 24fc1bfdf8..f21a9c69d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -22,10 +22,10 @@ import com.airbnb.mvrx.ViewModelContext 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.auth.data.LoginFlowTypes import im.vector.matrix.android.api.failure.toRegistrationFlowResponse import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.crosssigning.isVerified import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.rx.rx diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt index c3b645787f..b8aa490aa9 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -30,13 +30,13 @@ 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.NoOpMatrixCallback +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt index fa8ee16931..46ab694248 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -134,7 +134,7 @@ class VectorSettingsDevicesFragment @Inject constructor( val inflater = requireActivity().layoutInflater val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) - val input = layout.findViewById(R.id.edit_text) + val input = layout.findViewById(R.id.editText) input.setText(deviceInfo.displayName) AlertDialog.Builder(requireActivity()) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataFragment.kt index 7a57a03deb..ad8831124a 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/AccountDataFragment.kt @@ -29,7 +29,7 @@ import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider -import im.vector.riotx.core.utils.jsonViewerStyler +import im.vector.riotx.core.utils.createJSonViewerStyleProvider import kotlinx.android.synthetic.main.fragment_generic_recycler.* import org.billcarsonfr.jsonviewer.JSonViewerDialog import javax.inject.Inject @@ -73,7 +73,7 @@ class AccountDataFragment @Inject constructor( JSonViewerDialog.newInstance( jsonString, -1, // open All - jsonViewerStyler(colorProvider) + createJSonViewerStyleProvider(colorProvider) ).show(childFragmentManager, "JSON_VIEWER") } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailFragment.kt index d7ffd8adfa..9489fb8506 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailFragment.kt @@ -26,7 +26,7 @@ import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider -import im.vector.riotx.core.utils.jsonViewerStyler +import im.vector.riotx.core.utils.createJSonViewerStyleProvider import kotlinx.android.synthetic.main.fragment_generic_recycler.* import org.billcarsonfr.jsonviewer.JSonViewerDialog import javax.inject.Inject @@ -66,7 +66,7 @@ class GossipingEventsPaperTrailFragment @Inject constructor( JSonViewerDialog.newInstance( it, -1, - jsonViewerStyler(colorProvider) + createJSonViewerStyleProvider(colorProvider) ).show(childFragmentManager, "JSON_VIEWER") } } diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutActivity.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutActivity.kt index 96dbbaf102..88ddc85ac1 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutActivity.kt @@ -32,7 +32,6 @@ import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.login.LoginActivity -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_login.* import timber.log.Timber import javax.inject.Inject @@ -57,18 +56,11 @@ class SoftLogoutActivity : LoginActivity() { override fun initUiAndData() { super.initUiAndData() - softLogoutViewModel - .subscribe(this) { - updateWithState(it) - } + softLogoutViewModel.subscribe(this) { + updateWithState(it) + } - softLogoutViewModel.viewEvents - .observe() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - handleSoftLogoutViewEvents(it) - } - .disposeOnDestroy() + softLogoutViewModel.observeViewEvents { handleSoftLogoutViewEvents(it) } } private fun handleSoftLogoutViewEvents(softLogoutViewEvents: SoftLogoutViewEvents) { diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt index e2fe17b461..b1d5cb85c1 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt @@ -28,9 +28,9 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.data.LoginFlowResult +import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.hasUnsavedKeys import im.vector.riotx.core.platform.VectorViewModel @@ -105,9 +105,9 @@ class SoftLogoutViewModel @AssistedInject constructor( is LoginFlowResult.Success -> { val loginMode = when { // SSO login is taken first - data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso - data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { diff --git a/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt index 6a955999a4..3ffbe8c949 100644 --- a/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt +++ b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt @@ -23,8 +23,10 @@ import android.graphics.Bitmap import android.os.Build import android.webkit.WebResourceError import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient +import androidx.annotation.RequiresApi /** * This class inherits from WebViewClient. It has to be used with a WebView. @@ -56,6 +58,14 @@ class VectorWebViewClient(private val eventListener: WebViewEventListener) : Web } } + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) { + super.onReceivedHttpError(view, request, errorResponse) + eventListener.onHttpError(request.url.toString(), + errorResponse.statusCode, + errorResponse.reasonPhrase) + } + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) if (!mInError) { diff --git a/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt b/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt index f5026b48ca..92d9fedf94 100644 --- a/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt @@ -23,21 +23,27 @@ interface WebViewEventListener { * * @param url The url about to be rendered. */ - fun pageWillStart(url: String) + fun pageWillStart(url: String) { + // NO-OP + } /** * Triggered when a loading webview page has started. * * @param url The rendering url. */ - fun onPageStarted(url: String) + fun onPageStarted(url: String) { + // NO-OP + } /** * Triggered when a loading webview page has finished loading but has not been rendered yet. * * @param url The finished url. */ - fun onPageFinished(url: String) + fun onPageFinished(url: String) { + // NO-OP + } /** * Triggered when an error occurred while loading a page. @@ -46,7 +52,20 @@ interface WebViewEventListener { * @param errorCode The error code. * @param description The error description. */ - fun onPageError(url: String, errorCode: Int, description: String) + fun onPageError(url: String, errorCode: Int, description: String) { + // NO-OP + } + + /** + * Triggered when an error occurred while loading a page. + * + * @param url The url that failed. + * @param errorCode The error code. + * @param description The error description. + */ + fun onHttpError(url: String, errorCode: Int, description: String) { + // NO-OP + } /** * Triggered when a webview load an url @@ -54,5 +73,7 @@ interface WebViewEventListener { * @param url The url about to be rendered. * @return true if the method needs to manage some custom handling */ - fun shouldOverrideUrlLoading(url: String): Boolean + fun shouldOverrideUrlLoading(url: String): Boolean { + return false + } } diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetAPICallback.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetAPICallback.kt new file mode 100644 index 0000000000..3549ce979c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetAPICallback.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator +import im.vector.matrix.android.api.util.JsonDict +import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider + +class WidgetAPICallback(private val postAPIMediator: WidgetPostAPIMediator, + private val eventData: JsonDict, + private val stringProvider: StringProvider) : MatrixCallback { + + override fun onFailure(failure: Throwable) { + postAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData) + } + + override fun onSuccess(data: Any) { + postAPIMediator.sendSuccess(eventData) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetAction.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetAction.kt new file mode 100644 index 0000000000..06a27a7084 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetAction.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class WidgetAction : VectorViewModelAction { + data class OnWebViewStartedToLoad(val url: String) : WidgetAction() + data class OnWebViewLoadingError(val url: String, val isHttpError: Boolean, val errorCode: Int, val errorDescription: String) : WidgetAction() + data class OnWebViewLoadingSuccess(val url: String) : WidgetAction() + object LoadFormattedUrl : WidgetAction() + object DeleteWidget : WidgetAction() + object RevokeWidget : WidgetAction() + object OnTermsReviewed : WidgetAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetActivity.kt new file mode 100644 index 0000000000..a8153a65d8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetActivity.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.appcompat.widget.Toolbar +import androidx.core.view.isVisible +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.viewModel +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.platform.ToolbarConfigurable +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet +import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionViewEvents +import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionViewModel +import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionViewState +import kotlinx.android.synthetic.main.activity_widget.* +import java.io.Serializable +import javax.inject.Inject + +class WidgetActivity : VectorBaseActivity(), ToolbarConfigurable, WidgetViewModel.Factory, RoomWidgetPermissionViewModel.Factory { + + companion object { + + private const val WIDGET_FRAGMENT_TAG = "WIDGET_FRAGMENT_TAG" + private const val WIDGET_PERMISSION_FRAGMENT_TAG = "WIDGET_PERMISSION_FRAGMENT_TAG" + private const val EXTRA_RESULT = "EXTRA_RESULT" + + fun newIntent(context: Context, args: WidgetArgs): Intent { + return Intent(context, WidgetActivity::class.java).apply { + putExtra(MvRx.KEY_ARG, args) + } + } + + @Suppress("UNCHECKED_CAST") + fun getOutput(intent: Intent): Content? { + return intent.extras?.getSerializable(EXTRA_RESULT) as? Content + } + + fun createResultIntent(content: Content): Intent { + return Intent().apply { + putExtra(EXTRA_RESULT, content as Serializable) + } + } + } + + @Inject lateinit var viewModelFactory: WidgetViewModel.Factory + @Inject lateinit var permissionsViewModelFactory: RoomWidgetPermissionViewModel.Factory + + private val viewModel: WidgetViewModel by viewModel() + private val permissionViewModel: RoomWidgetPermissionViewModel by viewModel() + + override fun getLayoutRes() = R.layout.activity_widget + + override fun getMenuRes() = R.menu.menu_widget + + override fun getTitleRes() = R.string.room_widget_activity_title + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun initUiAndData() { + val widgetArgs: WidgetArgs? = intent?.extras?.getParcelable(MvRx.KEY_ARG) + if (widgetArgs == null) { + finish() + return + } + configure(toolbar) + toolbar.isVisible = widgetArgs.kind.nameRes != 0 + viewModel.observeViewEvents { + when (it) { + is WidgetViewEvents.Close -> handleClose(it) + } + } + + permissionViewModel.observeViewEvents { + when (it) { + is RoomWidgetPermissionViewEvents.Close -> finish() + } + } + + viewModel.selectSubscribe(this, WidgetViewState::status) { ws -> + when (ws) { + WidgetStatus.UNKNOWN -> { + } + WidgetStatus.WIDGET_NOT_ALLOWED -> { + val dFrag = supportFragmentManager.findFragmentByTag(WIDGET_PERMISSION_FRAGMENT_TAG) as? RoomWidgetPermissionBottomSheet + if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) { + return@selectSubscribe + } else { + RoomWidgetPermissionBottomSheet + .newInstance(widgetArgs) + .show(supportFragmentManager, WIDGET_PERMISSION_FRAGMENT_TAG) + } + } + WidgetStatus.WIDGET_ALLOWED -> { + if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) { + addFragment(R.id.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG) + } + } + } + } + + viewModel.selectSubscribe(this, WidgetViewState::widgetName) { name -> + supportActionBar?.title = name + } + + viewModel.selectSubscribe(this, WidgetViewState::canManageWidgets) { + invalidateOptionsMenu() + } + } + + override fun create(initialState: WidgetViewState): WidgetViewModel { + return viewModelFactory.create(initialState) + } + + override fun create(initialState: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel { + return permissionsViewModelFactory.create(initialState) + } + + private fun handleClose(event: WidgetViewEvents.Close) { + if (event.content != null) { + val intent = createResultIntent(event.content) + setResult(Activity.RESULT_OK, intent) + } + finish() + } + + override fun configure(toolbar: Toolbar) { + configureToolbar(toolbar) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetArgsBuilder.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetArgsBuilder.kt new file mode 100644 index 0000000000..6cbf1f29a1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetArgsBuilder.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets + +import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.riotx.core.di.ActiveSessionHolder +import javax.inject.Inject + +class WidgetArgsBuilder @Inject constructor(private val sessionHolder: ActiveSessionHolder) { + + @Suppress("UNCHECKED_CAST") + fun buildIntegrationManagerArgs(roomId: String, integId: String?, screen: String?): WidgetArgs { + val session = sessionHolder.getActiveSession() + val integrationManagerConfig = session.integrationManagerService().getPreferredConfig() + val normalizedScreen = when { + screen == null -> null + screen.startsWith("type_") -> screen + else -> "type_$screen" + } + return WidgetArgs( + baseUrl = integrationManagerConfig.uiUrl, + kind = WidgetKind.INTEGRATION_MANAGER, + roomId = roomId, + urlParams = mapOf( + "screen" to normalizedScreen, + "integ_id" to integId, + "room_id" to roomId + ).filterNotNull() + ) + } + + @Suppress("UNCHECKED_CAST") + fun buildStickerPickerArgs(roomId: String, widget: Widget): WidgetArgs { + val widgetId = widget.widgetId + val baseUrl = widget.computedUrl ?: throw IllegalStateException() + return WidgetArgs( + baseUrl = baseUrl, + kind = WidgetKind.STICKER_PICKER, + roomId = roomId, + widgetId = widgetId, + urlParams = mapOf( + "widgetId" to widgetId, + "room_id" to roomId + ).filterNotNull() + ) + } + + fun buildRoomWidgetArgs(roomId: String, widget: Widget): WidgetArgs { + val widgetId = widget.widgetId + val baseUrl = widget.computedUrl ?: throw IllegalStateException() + return WidgetArgs( + baseUrl = baseUrl, + kind = WidgetKind.ROOM, + roomId = roomId, + widgetId = widgetId + ) + } + + @Suppress("UNCHECKED_CAST") + private fun Map.filterNotNull(): Map { + return filterValues { it != null } as Map + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetFragment.kt new file mode 100644 index 0000000000..14b25d0439 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetFragment.kt @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets + +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.os.Parcelable +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.args +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.riotx.R +import im.vector.riotx.core.platform.OnBackPressed +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.openUrlInExternalBrowser +import im.vector.riotx.features.terms.ReviewTermsActivity +import im.vector.riotx.features.webview.WebViewEventListener +import im.vector.riotx.features.widgets.webview.clearAfterWidget +import im.vector.riotx.features.widgets.webview.setupForWidget +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_room_widget.* +import timber.log.Timber +import java.net.URISyntaxException +import javax.inject.Inject + +@Parcelize +data class WidgetArgs( + val baseUrl: String, + val kind: WidgetKind, + val roomId: String, + val widgetId: String? = null, + val urlParams: Map = emptyMap() +) : Parcelable + +class WidgetFragment @Inject constructor() : VectorBaseFragment(), WebViewEventListener, OnBackPressed { + + private val fragmentArgs: WidgetArgs by args() + private val viewModel: WidgetViewModel by activityViewModel() + + override fun getLayoutResId() = R.layout.fragment_room_widget + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setHasOptionsMenu(true) + widgetWebView.setupForWidget(this) + if (fragmentArgs.kind.isAdmin()) { + viewModel.getPostAPIMediator().setWebView(widgetWebView) + } + viewModel.observeViewEvents { + Timber.v("Observed view events: $it") + when (it) { + is WidgetViewEvents.DisplayTerms -> displayTerms(it) + is WidgetViewEvents.LoadFormattedURL -> loadFormattedUrl(it) + is WidgetViewEvents.DisplayIntegrationManager -> displayIntegrationManager(it) + is WidgetViewEvents.Failure -> displayErrorDialog(it.throwable) + } + } + viewModel.handle(WidgetAction.LoadFormattedUrl) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + viewModel.handle(WidgetAction.OnTermsReviewed) + } else { + vectorBaseActivity.finish() + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + if (fragmentArgs.kind.isAdmin()) { + viewModel.getPostAPIMediator().clearWebView() + } + widgetWebView.clearAfterWidget() + } + + override fun onResume() { + super.onResume() + widgetWebView?.let { + it.resumeTimers() + it.onResume() + } + } + + override fun onPause() { + super.onPause() + widgetWebView?.let { + it.pauseTimers() + it.onPause() + } + } + + override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state -> + val widget = state.asyncWidget() + menu.findItem(R.id.action_edit)?.isVisible = state.widgetKind != WidgetKind.INTEGRATION_MANAGER + if (widget == null) { + menu.findItem(R.id.action_refresh)?.isVisible = false + menu.findItem(R.id.action_widget_open_ext)?.isVisible = false + menu.findItem(R.id.action_delete)?.isVisible = false + menu.findItem(R.id.action_revoke)?.isVisible = false + } else { + menu.findItem(R.id.action_refresh)?.isVisible = true + menu.findItem(R.id.action_widget_open_ext)?.isVisible = true + menu.findItem(R.id.action_delete)?.isVisible = state.canManageWidgets && widget.isAddedByMe + menu.findItem(R.id.action_revoke)?.isVisible = state.status == WidgetStatus.WIDGET_ALLOWED && !widget.isAddedByMe + } + super.onPrepareOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { state -> + when (item.itemId) { + R.id.action_edit -> { + navigator.openIntegrationManager(requireContext(), state.roomId, state.widgetId, state.widgetKind.screenId) + return@withState true + } + R.id.action_delete -> { + viewModel.handle(WidgetAction.DeleteWidget) + return@withState true + } + R.id.action_refresh -> if (state.formattedURL.complete) { + widgetWebView.reload() + return@withState true + } + R.id.action_widget_open_ext -> if (state.formattedURL.complete) { + openUrlInExternalBrowser(requireContext(), state.formattedURL.invoke()) + return@withState true + } + R.id.action_revoke -> if (state.status == WidgetStatus.WIDGET_ALLOWED) { + viewModel.handle(WidgetAction.RevokeWidget) + return@withState true + } + } + return@withState super.onOptionsItemSelected(item) + } + + override fun onBackPressed(toolbarButton: Boolean): Boolean = withState(viewModel) { state -> + if (state.formattedURL.complete) { + if (widgetWebView.canGoBack()) { + widgetWebView.goBack() + return@withState true + } + } + return@withState false + } + + override fun invalidate() = withState(viewModel) { state -> + Timber.v("Invalidate state: $state") + when (state.formattedURL) { + is Incomplete -> { + setStateError(null) + widgetWebView.isInvisible = true + widgetProgressBar.isIndeterminate = true + widgetProgressBar.isVisible = true + } + is Success -> { + setStateError(null) + when (state.webviewLoadedUrl) { + Uninitialized -> { + widgetWebView.isInvisible = true + } + is Loading -> { + setStateError(null) + widgetWebView.isInvisible = false + widgetProgressBar.isIndeterminate = true + widgetProgressBar.isVisible = true + } + is Success -> { + widgetWebView.isInvisible = false + widgetProgressBar.isVisible = false + setStateError(null) + } + is Fail -> { + widgetProgressBar.isInvisible = true + setStateError(state.webviewLoadedUrl.error.message) + } + } + } + is Fail -> { + // we need to show Error + widgetWebView.isInvisible = true + widgetProgressBar.isVisible = false + setStateError(state.formattedURL.error.message) + } + } + } + + override fun shouldOverrideUrlLoading(url: String): Boolean { + if (url.startsWith("intent://")) { + try { + val context = requireContext() + val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME) + if (intent != null) { + val packageManager: PackageManager = context.packageManager + val info = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) + if (info != null) { + context.startActivity(intent) + } else { + val fallbackUrl = intent.getStringExtra("browser_fallback_url") + openUrlInExternalBrowser(context, fallbackUrl) + } + return true + } + } catch (e: URISyntaxException) { + Timber.d("Can't resolve intent://") + } + } + return false + } + + override fun onPageStarted(url: String) { + viewModel.handle(WidgetAction.OnWebViewStartedToLoad(url)) + } + + override fun onPageFinished(url: String) { + viewModel.handle(WidgetAction.OnWebViewLoadingSuccess(url)) + } + + override fun onPageError(url: String, errorCode: Int, description: String) { + viewModel.handle(WidgetAction.OnWebViewLoadingError(url, false, errorCode, description)) + } + + override fun onHttpError(url: String, errorCode: Int, description: String) { + viewModel.handle(WidgetAction.OnWebViewLoadingError(url, true, errorCode, description)) + } + + private fun displayTerms(displayTerms: WidgetViewEvents.DisplayTerms) { + navigator.openTerms( + fragment = this, + serviceType = TermsService.ServiceType.IntegrationManager, + baseUrl = displayTerms.url, + token = displayTerms.token + ) + } + + private fun loadFormattedUrl(loadFormattedUrl: WidgetViewEvents.LoadFormattedURL) { + widgetWebView.clearHistory() + widgetWebView.loadUrl(loadFormattedUrl.formattedURL) + } + + private fun setStateError(message: String?) { + if (message == null) { + widgetErrorLayout.isVisible = false + widgetErrorText.text = null + } else { + widgetProgressBar.isVisible = false + widgetErrorLayout.isVisible = true + widgetWebView.isInvisible = true + widgetErrorText.text = getString(R.string.room_widget_failed_to_load, message) + } + } + + private fun displayIntegrationManager(event: WidgetViewEvents.DisplayIntegrationManager) { + navigator.openIntegrationManager( + context = vectorBaseActivity, + roomId = fragmentArgs.roomId, + integId = event.integId, + screen = event.integType + ) + } + + fun deleteWidget() { + AlertDialog.Builder(requireContext()) + .setMessage(R.string.widget_delete_message_confirmation) + .setPositiveButton(R.string.remove) { _, _ -> + viewModel.handle(WidgetAction.DeleteWidget) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + fun revokeWidget() { + viewModel.handle(WidgetAction.RevokeWidget) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetPostAPIHandler.kt new file mode 100644 index 0000000000..351a15aad1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetPostAPIHandler.kt @@ -0,0 +1,460 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets + +import android.text.TextUtils +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.Content +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.toContent +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.PowerLevelsContent +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper +import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider +import timber.log.Timber +import java.util.ArrayList +import java.util.HashMap + +class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roomId: String, + @Assisted private val navigationCallback: NavigationCallback, + private val stringProvider: StringProvider, + private val session: Session) : WidgetPostAPIMediator.Handler { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String, navigationCallback: NavigationCallback): WidgetPostAPIHandler + } + + interface NavigationCallback { + fun close() + fun closeWithResult(content: Content) + fun openIntegrationManager(integId: String?, integType: String?) + } + + private val widgetPostAPIMediator = session.widgetService().getWidgetPostAPIMediator() + private val room = session.getRoom(roomId)!! + + override fun handleWidgetRequest(eventData: JsonDict): Boolean { + return when (eventData["action"] as String?) { + "integration_manager_open" -> handleIntegrationManagerOpenAction(eventData).run { true } + "bot_options" -> getBotOptions(eventData).run { true } + "can_send_event" -> canSendEvent(eventData).run { true } + "close_scalar" -> handleCloseScalar().run { true } + "get_membership_count" -> getMembershipCount(eventData).run { true } + "get_widgets" -> getWidgets(eventData).run { true } + "invite" -> inviteUser(eventData).run { true } + "join_rules_state" -> getJoinRules(eventData).run { true } + "membership_state" -> getMembershipState(eventData).run { true } + "set_bot_options" -> setBotOptions(eventData).run { true } + "set_bot_power" -> setBotPower(eventData).run { true } + "set_plumbing_state" -> setPlumbingState(eventData).run { true } + "set_widget" -> setWidget(eventData).run { true } + "m.sticker" -> pickStickerData(eventData).run { true } + else -> false + } + } + + private fun handleCloseScalar() { + navigationCallback.close() + } + + private fun handleIntegrationManagerOpenAction(eventData: JsonDict) { + var integType: String? = null + var integId: String? = null + val data = eventData["data"] + data + .takeIf { it is Map<*, *> } + ?.let { + val dict = data as Map<*, *> + + dict["integType"] + .takeIf { it is String } + ?.let { integType = it as String } + + dict["integId"] + .takeIf { it is String } + ?.let { integId = it as String } + + // Add "type_" as a prefix + integType?.let { integType = "type_$integType" } + } + navigationCallback.openIntegrationManager(integId, integType) + } + + /** + * Retrieve the latest botOptions event + * + * @param eventData the modular data + */ + private fun getBotOptions(eventData: JsonDict) { + if (checkRoomId(eventData) || checkUserId(eventData)) { + return + } + val userId = eventData["user_id"] as String + Timber.d("Received request to get options for bot $userId in room $roomId requested") + val stateEvents = room.getStateEvents(setOf(EventType.BOT_OPTIONS)) + var botOptionsEvent: Event? = null + val stateKey = "_$userId" + for (stateEvent in stateEvents) { + if (TextUtils.equals(stateEvent.stateKey, stateKey)) { + if (null == botOptionsEvent || stateEvent.ageLocalTs ?: 0 > botOptionsEvent.ageLocalTs ?: 0) { + botOptionsEvent = stateEvent + } + } + } + if (null != botOptionsEvent) { + Timber.d("Received request to get options for bot $userId returns $botOptionsEvent") + widgetPostAPIMediator.sendObjectResponse(Event::class.java, botOptionsEvent, eventData) + } else { + Timber.d("Received request to get options for bot $userId returns null") + widgetPostAPIMediator.sendObjectResponse(Event::class.java, null, eventData) + } + } + + private fun canSendEvent(eventData: JsonDict) { + if (checkRoomId(eventData)) { + return + } + Timber.d("Received request canSendEvent in room $roomId") + if (room.roomSummary()?.membership != Membership.JOIN) { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_must_be_in_room), eventData) + return + } + + val eventType = eventData["event_type"] as String + val isState = eventData["is_state"] as Boolean + + Timber.d("## canSendEvent() : eventType $eventType isState $isState") + + val powerLevelsEvent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + val powerLevelsContent = powerLevelsEvent?.content?.toModel() + val canSend = if (powerLevelsContent == null) { + false + } else { + PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(session.myUserId, isState, eventType) + } + if (canSend) { + Timber.d("## canSendEvent() returns true") + widgetPostAPIMediator.sendBoolResponse(true, eventData) + } else { + Timber.d("## canSendEvent() returns widget_integration_no_permission_in_room") + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_no_permission_in_room), eventData) + } + } + + /** + * Provides the membership state + * + * @param eventData the modular data + */ + private fun getMembershipState(eventData: JsonDict) { + if (checkRoomId(eventData) || checkUserId(eventData)) { + return + } + val userId = eventData["user_id"] as String + Timber.d("membership_state of $userId in room $roomId requested") + val roomMemberStateEvent = room.getStateEvent(EventType.STATE_ROOM_MEMBER, stateKey = QueryStringValue.Equals(userId, QueryStringValue.Case.SENSITIVE)) + if (roomMemberStateEvent != null) { + widgetPostAPIMediator.sendObjectResponse(Map::class.java, roomMemberStateEvent.content, eventData) + } else { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData) + } + } + + /** + * Request the latest joined room event + * + * @param eventData the modular data + */ + private fun getJoinRules(eventData: JsonDict) { + if (checkRoomId(eventData)) { + return + } + Timber.d("Received request join rules in room $roomId") + val joinedEvents = room.getStateEvents(setOf(EventType.STATE_ROOM_JOIN_RULES)) + if (joinedEvents.isNotEmpty()) { + widgetPostAPIMediator.sendObjectResponse(Event::class.java, joinedEvents.last(), eventData) + } else { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData) + } + } + + /** + * Provide the widgets list + * + * @param eventData the modular data + */ + private fun getWidgets(eventData: JsonDict) { + if (checkRoomId(eventData)) { + return + } + Timber.d("Received request to get widget in room $roomId") + val responseData = ArrayList() + val allWidgets = session.widgetService().getRoomWidgets(roomId) + session.widgetService().getUserWidgets() + for (widget in allWidgets) { + val map = widget.event.toContent() + responseData.add(map) + } + Timber.d("## getWidgets() returns $responseData") + widgetPostAPIMediator.sendObjectResponse(List::class.java, responseData, eventData) + } + + /** + * Set a new widget + * + * @param eventData the modular data + */ + private fun setWidget(eventData: JsonDict) { + val userWidget = eventData["userWidget"] as Boolean? + if (userWidget == true) { + Timber.d("Received request to set widget for user") + } else { + if (checkRoomId(eventData)) { + return + } + Timber.d("Received request to set widget in room $roomId") + } + val widgetId = eventData["widget_id"] as String? + val widgetType = eventData["type"] as String? + val widgetUrl = eventData["url"] as String? + + // optional + val widgetName = eventData["name"] as String? + // optional + val widgetData = eventData["data"] + if (widgetId == null) { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_unable_to_create), eventData) + return + } + + val widgetEventContent = HashMap() + + if (null != widgetUrl) { + if (null == widgetType) { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_unable_to_create), eventData) + return + } + + widgetEventContent["type"] = widgetType + widgetEventContent["url"] = widgetUrl + + if (null != widgetName) { + widgetEventContent["name"] = widgetName + } + + if (null != widgetData) { + widgetEventContent["data"] = widgetData + } + } + + if (userWidget == true) { + val addUserWidgetBody = mapOf( + widgetId to mapOf( + "content" to widgetEventContent, + "state_key" to widgetId, + "id" to widgetId, + "sender" to session.myUserId, + "type" to "m.widget" + ) + ) + session.updateAccountData( + type = UserAccountData.TYPE_WIDGETS, + content = addUserWidgetBody, + callback = createWidgetAPICallback(eventData) + ) + } else { + session.widgetService().createRoomWidget( + roomId = roomId, + widgetId = widgetId, + content = widgetEventContent, + callback = createWidgetAPICallback(eventData) + ) + } + } + + /** + * Update the 'plumbing state" + * + * @param eventData the modular data + */ + private fun setPlumbingState(eventData: JsonDict) { + if (checkRoomId(eventData)) { + return + } + val description = "Received request to set plumbing state to status " + eventData["status"] + " in room " + roomId + " requested" + Timber.d(description) + + val status = eventData["status"] as String + + val params = HashMap() + params["status"] = status + room.sendStateEvent( + eventType = EventType.PLUMBING, + stateKey = null, + body = params, + callback = createWidgetAPICallback(eventData) + ) + } + + /** + * Update the bot options + * + * @param eventData the modular data + */ + @Suppress("UNCHECKED_CAST") + private fun setBotOptions(eventData: JsonDict) { + if (checkRoomId(eventData) || checkUserId(eventData)) { + return + } + val userId = eventData["user_id"] as String + val description = "Received request to set options for bot $userId in room $roomId" + Timber.d(description) + val content = eventData["content"] as JsonDict + val stateKey = "_$userId" + room.sendStateEvent( + eventType = EventType.BOT_OPTIONS, + stateKey = stateKey, + body = content, + callback = createWidgetAPICallback(eventData) + ) + } + + /** + * Update the bot power levels + * + * @param eventData the modular data + */ + private fun setBotPower(eventData: JsonDict) { + if (checkRoomId(eventData) || checkUserId(eventData)) { + return + } + val userId = eventData["user_id"] as String + val description = "Received request to set power level to " + eventData["level"] + " for bot " + userId + " in room " + roomId + Timber.d(description) + val level = eventData["level"] as Int + if (level >= 0) { + // TODO + // room.updateUserPowerLevels(userId, level, WidgetApiCallback(eventData, description)) + } else { + Timber.e("## setBotPower() : Power level must be positive integer.") + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_positive_power_level), eventData) + } + } + + /** + * Invite an user to this room + * + * @param eventData the modular data + */ + private fun inviteUser(eventData: JsonDict) { + if (checkRoomId(eventData) || checkUserId(eventData)) { + return + } + val userId = eventData["user_id"] as String + val description = "Received request to invite $userId into room $roomId" + Timber.d(description) + val member = room.getRoomMember(userId) + if (member != null && member.membership == Membership.JOIN) { + widgetPostAPIMediator.sendSuccess(eventData) + } else { + room.invite(userId = userId, callback = createWidgetAPICallback(eventData)) + } + } + + /** + * Provides the number of members in the rooms + * + * @param eventData the modular data + */ + private fun getMembershipCount(eventData: JsonDict) { + if (checkRoomId(eventData)) { + return + } + val numberOfJoinedMembers = room.getNumberOfJoinedMembers() + widgetPostAPIMediator.sendIntegerResponse(numberOfJoinedMembers, eventData) + } + + @Suppress("UNCHECKED_CAST") + private fun pickStickerData(eventData: JsonDict) { + Timber.d("Received request send sticker") + val data = eventData["data"] + if (data == null) { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_missing_parameter), eventData) + return + } + val content = (data as? JsonDict)?.get("content") as? Content + if (content == null) { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_missing_parameter), eventData) + return + } + widgetPostAPIMediator.sendSuccess(eventData) + navigationCallback.closeWithResult(content) + } + + /** + * Check if roomId is present in the event and match + * Send response and return true in case of error + * + * @return true in case of error + */ + private fun checkRoomId(eventData: JsonDict): Boolean { + val roomIdInEvent = eventData["room_id"] as String? + // Check if param is present + if (null == roomIdInEvent) { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_missing_room_id), eventData) + return true + } + + if (!TextUtils.equals(roomIdInEvent, roomId)) { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_room_not_visible), eventData) + return true + } + + // OK + return false + } + + /** + * Check if userId is present in the event + * Send response and return true in case of error + * + * @return true in case of error + */ + private fun checkUserId(eventData: JsonDict): Boolean { + val userIdInEvent = eventData["user_id"] as String? + // Check if param is present + if (null == userIdInEvent) { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_missing_user_id), eventData) + return true + } + // OK + return false + } + + private fun createWidgetAPICallback(eventData: JsonDict): WidgetAPICallback { + return WidgetAPICallback(widgetPostAPIMediator, eventData, stringProvider) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewEvents.kt new file mode 100644 index 0000000000..7750f2dd68 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewEvents.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets + +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class WidgetViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable): WidgetViewEvents() + data class Close(val content: Content? = null) : WidgetViewEvents() + data class DisplayIntegrationManager(val integId: String?, val integType: String?) : WidgetViewEvents() + data class LoadFormattedURL(val formattedURL: String) : WidgetViewEvents() + data class DisplayTerms(val url: String, val token: String) : WidgetViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt new file mode 100644 index 0000000000..d81e1efea4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.Content +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.integrationmanager.IntegrationManagerService +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper +import im.vector.matrix.android.api.session.widgets.WidgetManagementFailure +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.matrix.rx.mapOptional +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.unwrap +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.widgets.permissions.WidgetPermissionsHelper +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.net.ssl.HttpsURLConnection + +class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: WidgetViewState, + private val widgetPostAPIHandlerFactory: WidgetPostAPIHandler.Factory, + private val stringProvider: StringProvider, + private val session: Session) + : VectorViewModel(initialState), + WidgetPostAPIHandler.NavigationCallback, + IntegrationManagerService.Listener { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: WidgetViewState): WidgetViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: WidgetViewState): WidgetViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + private val room = session.getRoom(initialState.roomId) + private val widgetService = session.widgetService() + private val integrationManagerService = session.integrationManagerService() + private val widgetURLFormatter = widgetService.getWidgetURLFormatter() + private val postAPIMediator = widgetService.getWidgetPostAPIMediator() + + init { + integrationManagerService.addListener(this) + if (initialState.widgetKind.isAdmin()) { + val widgetPostAPIHandler = widgetPostAPIHandlerFactory.create(initialState.roomId, this) + postAPIMediator.setHandler(widgetPostAPIHandler) + } + setupName() + refreshPermissionStatus() + observePowerLevel() + observeWidgetIfNeeded() + subscribeToWidget() + } + + private fun subscribeToWidget() { + asyncSubscribe(WidgetViewState::asyncWidget) { + setState { copy(widgetName = it.name) } + } + } + + private fun setupName() { + val nameRes = initialState.widgetKind.nameRes + if (nameRes != 0) { + val name = stringProvider.getString(nameRes) + setState { copy(widgetName = name) } + } + } + + private fun observePowerLevel() { + if (room == null) { + return + } + room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .map { + PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, null) + }.subscribe { + setState { copy(canManageWidgets = it) } + }.disposeOnClear() + } + + private fun observeWidgetIfNeeded() { + if (initialState.widgetKind != WidgetKind.ROOM) { + return + } + val widgetId = initialState.widgetId ?: return + session.rx() + .liveRoomWidgets(initialState.roomId, QueryStringValue.Equals(widgetId)) + .filter { it.isNotEmpty() } + .map { it.first() } + .execute { + copy(asyncWidget = it) + } + } + + fun getPostAPIMediator() = postAPIMediator + + override fun handle(action: WidgetAction) { + when (action) { + is WidgetAction.OnWebViewLoadingError -> handleWebViewLoadingError(action) + is WidgetAction.OnWebViewLoadingSuccess -> handleWebViewLoadingSuccess(action) + is WidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading() + WidgetAction.LoadFormattedUrl -> loadFormattedUrl() + WidgetAction.DeleteWidget -> handleDeleteWidget() + WidgetAction.RevokeWidget -> handleRevokeWidget() + WidgetAction.OnTermsReviewed -> refreshPermissionStatus() + } + } + + private fun handleRevokeWidget() { + viewModelScope.launch { + val widgetId = initialState.widgetId ?: return@launch + try { + WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(initialState.roomId, widgetId, false) + _viewEvents.post(WidgetViewEvents.Close()) + } catch (failure: Throwable) { + _viewEvents.post(WidgetViewEvents.Failure(failure)) + } + } + } + + private fun handleDeleteWidget() { + viewModelScope.launch { + val widgetId = initialState.widgetId ?: return@launch + try { + awaitCallback { + widgetService.destroyRoomWidget(initialState.roomId, widgetId, it) + _viewEvents.post(WidgetViewEvents.Close()) + } + } catch (failure: Throwable) { + _viewEvents.post(WidgetViewEvents.Failure(failure)) + } + } + } + + private fun refreshPermissionStatus() { + if (initialState.widgetKind.isAdmin()) { + setWidgetStatus(WidgetStatus.WIDGET_ALLOWED) + } else { + val widgetId = initialState.widgetId + if (widgetId == null) { + setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED) + return + } + val roomWidget = widgetService.getRoomWidgets( + roomId = initialState.roomId, + widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.SENSITIVE) + ).firstOrNull() + if (roomWidget == null) { + setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED) + return + } + if (roomWidget.event.senderId == session.myUserId) { + setWidgetStatus(WidgetStatus.WIDGET_ALLOWED) + } else { + val stateEventId = roomWidget.event.eventId + // This should not happen + if (stateEventId == null) { + setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED) + return + } + val isAllowed = integrationManagerService.isWidgetAllowed(stateEventId) + if (!isAllowed) { + setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED) + } else { + setWidgetStatus(WidgetStatus.WIDGET_ALLOWED) + } + } + } + } + + private fun setWidgetStatus(widgetStatus: WidgetStatus) { + setState { copy(status = widgetStatus) } + } + + private fun loadFormattedUrl(forceFetchToken: Boolean = false) { + viewModelScope.launch { + try { + setState { copy(formattedURL = Loading()) } + val formattedUrl = widgetURLFormatter.format( + baseUrl = initialState.baseUrl, + params = initialState.urlParams, + forceFetchScalarToken = forceFetchToken, + bypassWhitelist = initialState.widgetKind == WidgetKind.INTEGRATION_MANAGER + ) + setState { copy(formattedURL = Success(formattedUrl)) } + Timber.v("Post load formatted url event: $formattedUrl") + _viewEvents.post(WidgetViewEvents.LoadFormattedURL(formattedUrl)) + } catch (failure: Throwable) { + if (failure is WidgetManagementFailure.TermsNotSignedException) { + _viewEvents.post(WidgetViewEvents.DisplayTerms(failure.baseUrl, failure.token)) + } + setState { copy(formattedURL = Fail(failure)) } + } + } + } + + private fun handleWebViewStartLoading() { + setState { copy(webviewLoadedUrl = Loading()) } + } + + private fun handleWebViewLoadingSuccess(action: WidgetAction.OnWebViewLoadingSuccess) { + if (initialState.widgetKind.isAdmin()) { + postAPIMediator.injectAPI() + } + setState { copy(webviewLoadedUrl = Success(action.url)) } + } + + private fun handleWebViewLoadingError(action: WidgetAction.OnWebViewLoadingError) = withState { + if (!action.url.startsWith(it.baseUrl)) { + return@withState + } + if (action.isHttpError) { + // In case of 403, try to refresh the scalar token + if (it.formattedURL is Success && action.errorCode == HttpsURLConnection.HTTP_FORBIDDEN) { + loadFormattedUrl(true) + } + } else { + setState { copy(webviewLoadedUrl = Fail(Throwable(action.errorDescription))) } + } + } + + override fun onCleared() { + integrationManagerService.removeListener(this) + postAPIMediator.setHandler(null) + super.onCleared() + } + +// IntegrationManagerService.Listener + + override fun onWidgetPermissionsChanged(widgets: Map) { + refreshPermissionStatus() + } + +// WidgetPostAPIHandler.NavigationCallback + + override fun close() { + _viewEvents.post(WidgetViewEvents.Close(null)) + } + + override fun closeWithResult(content: Content) { + _viewEvents.post(WidgetViewEvents.Close(content)) + } + + override fun openIntegrationManager(integId: String?, integType: String?) { + _viewEvents.post(WidgetViewEvents.DisplayIntegrationManager(integId, integType)) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewState.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewState.kt new file mode 100644 index 0000000000..2242efc82f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewState.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets + +import androidx.annotation.StringRes +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.matrix.android.api.session.widgets.model.WidgetType +import im.vector.riotx.R + +enum class WidgetStatus { + UNKNOWN, + WIDGET_NOT_ALLOWED, + WIDGET_ALLOWED +} + +enum class WidgetKind(@StringRes val nameRes: Int, val screenId: String?) { + ROOM(R.string.room_widget_activity_title, null), + STICKER_PICKER(R.string.title_activity_choose_sticker, WidgetType.StickerPicker.preferred), + INTEGRATION_MANAGER(0, null); + + fun isAdmin(): Boolean { + return this == STICKER_PICKER || this == INTEGRATION_MANAGER + } +} + +data class WidgetViewState( + val roomId: String, + val baseUrl: String, + val urlParams: Map = emptyMap(), + val widgetId: String? = null, + val widgetKind: WidgetKind, + val status: WidgetStatus = WidgetStatus.UNKNOWN, + val formattedURL: Async = Uninitialized, + val webviewLoadedUrl: Async = Uninitialized, + val widgetName: String = "", + val canManageWidgets: Boolean = false, + val asyncWidget: Async = Uninitialized +) : MvRxState { + + constructor(widgetArgs: WidgetArgs) : this( + widgetKind = widgetArgs.kind, + baseUrl = widgetArgs.baseUrl, + roomId = widgetArgs.roomId, + widgetId = widgetArgs.widgetId, + urlParams = widgetArgs.urlParams + ) +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionActions.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionActions.kt new file mode 100644 index 0000000000..280da5a757 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionActions.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets.permissions + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RoomWidgetPermissionActions : VectorViewModelAction { + object AllowWidget: RoomWidgetPermissionActions() + object BlockWidget: RoomWidgetPermissionActions() + object DoClose: RoomWidgetPermissionActions() +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt new file mode 100644 index 0000000000..f3a9e5fda6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.widgets.permissions + +import android.content.DialogInterface +import android.os.Build +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.BulletSpan +import butterknife.OnClick +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.withArgs +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.widgets.WidgetArgs +import kotlinx.android.synthetic.main.bottom_sheet_room_widget_permission.* +import javax.inject.Inject + +class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() { + + override fun getLayoutResId(): Int = R.layout.bottom_sheet_room_widget_permission + + private val viewModel: RoomWidgetPermissionViewModel by activityViewModel() + + @Inject lateinit var avatarRenderer: AvatarRenderer + + override val showExpanded = true + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun invalidate() = withState(viewModel) { state -> + super.invalidate() + val permissionData = state.permissionData() ?: return@withState + widgetPermissionOwnerId.text = permissionData.widget.senderInfo?.userId ?: "" + widgetPermissionOwnerDisplayName.text = permissionData.widget.senderInfo?.disambiguatedDisplayName + permissionData.widget.senderInfo?.toMatrixItem()?.also { + avatarRenderer.render(it, widgetPermissionOwnerAvatar) + } + + val domain = permissionData.widgetDomain ?: "" + val infoBuilder = SpannableStringBuilder() + .append(getString( + R.string.room_widget_permission_webview_shared_info_title + .takeIf { permissionData.isWebviewWidget } + ?: R.string.room_widget_permission_shared_info_title, + "'$domain'")) + infoBuilder.append("\n") + permissionData.permissionsList.forEach { + infoBuilder.append("\n") + val bulletPoint = getString(it) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + infoBuilder.append(bulletPoint, BulletSpan(resources.getDimension(R.dimen.quote_gap).toInt()), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + val start = infoBuilder.length + infoBuilder.append(bulletPoint) + infoBuilder.setSpan( + BulletSpan(resources.getDimension(R.dimen.quote_gap).toInt()), + start, + bulletPoint.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + infoBuilder.append("\n") + widgetPermissionSharedInfo.text = infoBuilder + } + + @OnClick(R.id.widgetPermissionDecline) + fun doDecline() { + viewModel.handle(RoomWidgetPermissionActions.BlockWidget) + // optimistic dismiss + dismiss() + } + + @OnClick(R.id.widgetPermissionContinue) + fun doAccept() { + viewModel.handle(RoomWidgetPermissionActions.AllowWidget) + // optimistic dismiss + dismiss() + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + viewModel.handle(RoomWidgetPermissionActions.DoClose) + } + + companion object { + + fun newInstance(widgetArgs: WidgetArgs) = RoomWidgetPermissionBottomSheet().withArgs { + putParcelable(MvRx.KEY_ARG, widgetArgs) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsConstants.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewEvents.kt similarity index 62% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsConstants.kt rename to vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewEvents.kt index 16af3d216e..c3138d082a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsConstants.kt +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewEvents.kt @@ -1,25 +1,23 @@ /* - * Copyright 2020 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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.powerlevels +package im.vector.riotx.features.widgets.permissions -object PowerLevelsConstants { +import im.vector.riotx.core.platform.VectorViewEvents - const val DEFAULT_ROOM_ADMIN_LEVEL = 100 - const val DEFAULT_ROOM_MODERATOR_LEVEL = 50 - const val DEFAULT_ROOM_USER_LEVEL = 0 +sealed class RoomWidgetPermissionViewEvents : VectorViewEvents { + object Close : RoomWidgetPermissionViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewModel.kt new file mode 100644 index 0000000000..8e52a0c7d3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewModel.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.widgets.permissions + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +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.extensions.orFalse +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.rx.rx +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorViewModel +import kotlinx.coroutines.launch +import timber.log.Timber +import java.net.URL + +class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val initialState: RoomWidgetPermissionViewState, + private val session: Session) + : VectorViewModel(initialState) { + + private val widgetService = session.widgetService() + private val integrationManagerService = session.integrationManagerService() + + init { + observeWidget() + } + + private fun observeWidget() { + val widgetId = initialState.widgetId ?: return + session.rx() + .liveRoomWidgets(initialState.roomId, QueryStringValue.Equals(widgetId)) + .filter { it.isNotEmpty() } + .map { + val widget = it.first() + val domain = try { + URL(widget.computedUrl).host + } catch (e: Throwable) { + null + } + // TODO check from widget urls the perms that should be shown? + // For now put all + val infoShared = listOf( + R.string.room_widget_permission_display_name, + R.string.room_widget_permission_avatar_url, + R.string.room_widget_permission_user_id, + R.string.room_widget_permission_theme, + R.string.room_widget_permission_widget_id, + R.string.room_widget_permission_room_id + ) + RoomWidgetPermissionViewState.WidgetPermissionData( + widget = widget, + isWebviewWidget = true, + permissionsList = infoShared, + widgetDomain = domain + ) + } + .execute { + copy(permissionData = it) + } + } + + override fun handle(action: RoomWidgetPermissionActions) { + when (action) { + RoomWidgetPermissionActions.AllowWidget -> handleAllowWidget() + RoomWidgetPermissionActions.BlockWidget -> handleRevokeWidget() + } + } + + private fun handleRevokeWidget() = withState { state -> + viewModelScope.launch { + try { + val widgetId = state.widgetId ?: return@launch + if (state.permissionData()?.isWebviewWidget.orFalse()) { + WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, false) + } else { + // TODO JITSI + } + } catch (failure: Throwable) { + Timber.v("Failure revoking widget: ${state.widgetId}") + } finally { + // We send close event in every situation + _viewEvents.post(RoomWidgetPermissionViewEvents.Close) + } + } + } + + private fun handleAllowWidget() = withState { state -> + viewModelScope.launch { + try { + val widgetId = state.widgetId ?: return@launch + if (state.permissionData()?.isWebviewWidget.orFalse()) { + WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, true) + } else { + // TODO JITSI + } + } catch (failure: Throwable) { + Timber.v("Failure allowing widget: ${state.widgetId}") + // We send close event only when it's failed + _viewEvents.post(RoomWidgetPermissionViewEvents.Close) + } + } + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewState.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewState.kt new file mode 100644 index 0000000000..8daef86324 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewState.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets.permissions + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.riotx.features.widgets.WidgetArgs + +data class RoomWidgetPermissionViewState( + val roomId: String, + val widgetId: String?, + val permissionData: Async = Uninitialized +) : MvRxState { + + constructor(widgetArgs: WidgetArgs) : this( + roomId = widgetArgs.roomId, + widgetId = widgetArgs.widgetId + ) + + data class WidgetPermissionData( + val widget: Widget, + val permissionsList: List = emptyList(), + val isWebviewWidget: Boolean = true, + val widgetDomain: String? = null + ) +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/permissions/WidgetPermissionsHelper.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/WidgetPermissionsHelper.kt new file mode 100644 index 0000000000..bcf5f2c541 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/WidgetPermissionsHelper.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets.permissions + +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService +import im.vector.matrix.android.api.session.widgets.WidgetService +import im.vector.matrix.android.internal.util.awaitCallback + +class WidgetPermissionsHelper(private val integrationManagerService: IntegrationManagerService, + private val widgetService: WidgetService) { + + suspend fun changePermission(roomId: String, widgetId: String, allow: Boolean) { + val widget = widgetService.getRoomWidgets( + roomId = roomId, + widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.SENSITIVE) + ).firstOrNull() + val eventId = widget?.event?.eventId ?: return + awaitCallback { + integrationManagerService.setWidgetAllowed(eventId, allow, it) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/webview/WebviewPermissionUtils.kt b/vector/src/main/java/im/vector/riotx/features/widgets/webview/WebviewPermissionUtils.kt new file mode 100644 index 0000000000..32f6a906e2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/webview/WebviewPermissionUtils.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.fragments.roomwidgets + +import android.annotation.SuppressLint +import android.content.Context +import android.webkit.PermissionRequest +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import im.vector.riotx.R + +object WebviewPermissionUtils { + + @SuppressLint("NewApi") + fun promptForPermissions(@StringRes title: Int, request: PermissionRequest, context: Context) { + val allowedPermissions = request.resources.map { + it to false + }.toMutableList() + AlertDialog.Builder(context) + .setTitle(title) + .setMultiChoiceItems( + request.resources.map { webPermissionToHumanReadable(it, context) }.toTypedArray(), null + ) { _, which, isChecked -> + allowedPermissions[which] = allowedPermissions[which].first to isChecked + } + .setPositiveButton(R.string.room_widget_resource_grant_permission) { _, _ -> + request.grant(allowedPermissions.mapNotNull { perm -> + perm.first.takeIf { perm.second } + }.toTypedArray()) + } + .setNegativeButton(R.string.room_widget_resource_decline_permission) { _, _ -> + request.deny() + } + .show() + } + + private fun webPermissionToHumanReadable(permission: String, context: Context): String { + return when (permission) { + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> context.getString(R.string.room_widget_webview_access_microphone) + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> context.getString(R.string.room_widget_webview_access_camera) + PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> context.getString(R.string.room_widget_webview_read_protected_media) + else -> permission + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/riotx/features/widgets/webview/WidgetWebView.kt new file mode 100644 index 0000000000..68cbe76531 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/webview/WidgetWebView.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.widgets.webview + +import android.annotation.SuppressLint +import android.os.Build +import android.view.ViewGroup +import android.webkit.CookieManager +import android.webkit.PermissionRequest +import android.webkit.WebChromeClient +import android.webkit.WebSettings +import android.webkit.WebView +import im.vector.fragments.roomwidgets.WebviewPermissionUtils +import im.vector.riotx.R +import im.vector.riotx.features.themes.ThemeUtils +import im.vector.riotx.features.webview.VectorWebViewClient +import im.vector.riotx.features.webview.WebViewEventListener + +@SuppressLint("NewApi") +fun WebView.setupForWidget(webViewEventListener: WebViewEventListener) { + // xml value seems ignored + setBackgroundColor(ThemeUtils.getColor(context, R.attr.vctr_bottom_nav_background_color)) + + // clear caches + clearHistory() + clearFormData() + clearCache(true) + + // does not cache the data + settings.cacheMode = WebSettings.LOAD_NO_CACHE + + // Enable Javascript + settings.javaScriptEnabled = true + + // Use WideViewport and Zoom out if there is no viewport defined + settings.useWideViewPort = true + settings.loadWithOverviewMode = true + + // Enable pinch to zoom without the zoom buttons + settings.builtInZoomControls = true + + // Allow use of Local Storage + settings.domStorageEnabled = true + + settings.allowFileAccessFromFileURLs = true + settings.allowUniversalAccessFromFileURLs = true + + settings.displayZoomControls = false + + // Permission requests + webChromeClient = object : WebChromeClient() { + override fun onPermissionRequest(request: PermissionRequest) { + WebviewPermissionUtils.promptForPermissions(R.string.room_widget_resource_permission_title, request, context) + } + } + webViewClient = VectorWebViewClient(webViewEventListener) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val cookieManager = CookieManager.getInstance() + cookieManager.setAcceptThirdPartyCookies(this, false) + } +} + +fun WebView.clearAfterWidget() { + // Make sure you remove the WebView from its parent view before doing anything. + (parent as? ViewGroup)?.removeAllViews() + webChromeClient = null + webViewClient = null + clearHistory() + + // NOTE: clears RAM cache, if you pass true, it will also clear the disk cache. + clearCache(true) + + // Loading a blank page is optional, but will ensure that the WebView isn't doing anything when you destroy it. + loadUrl("about:blank") + + onPause() + removeAllViews() + + // NOTE: This pauses JavaScript execution for ALL WebViews, + // do not use if you have other WebViews still alive. + // If you create another WebView after calling this, + // make sure to call mWebView.resumeTimers(). + pauseTimers() + + // NOTE: This can occasionally cause a segfault below API 17 (4.2) + destroy() +} diff --git a/vector/src/main/res/drawable/bg_active_widgets_banner.xml b/vector/src/main/res/drawable/bg_active_widgets_banner.xml new file mode 100644 index 0000000000..347f6ef28e --- /dev/null +++ b/vector/src/main/res/drawable/bg_active_widgets_banner.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_back_24dp.xml b/vector/src/main/res/drawable/ic_back_24dp.xml new file mode 100644 index 0000000000..db9c94314d --- /dev/null +++ b/vector/src/main/res/drawable/ic_back_24dp.xml @@ -0,0 +1,23 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_star_24dp.xml b/vector/src/main/res/drawable/ic_star_24dp.xml new file mode 100644 index 0000000000..3113cf8f47 --- /dev/null +++ b/vector/src/main/res/drawable/ic_star_24dp.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_star_green_24dp.xml b/vector/src/main/res/drawable/ic_star_green_24dp.xml new file mode 100644 index 0000000000..1fd974386f --- /dev/null +++ b/vector/src/main/res/drawable/ic_star_green_24dp.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/layout/activity_widget.xml b/vector/src/main/res/layout/activity_widget.xml new file mode 100755 index 0000000000..047bcbbc7c --- /dev/null +++ b/vector/src/main/res/layout/activity_widget.xml @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml b/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml new file mode 100644 index 0000000000..1aa77a6c3b --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/dialog_base_edit_text.xml b/vector/src/main/res/layout/dialog_base_edit_text.xml index 45c55f4451..98dc398042 100644 --- a/vector/src/main/res/layout/dialog_base_edit_text.xml +++ b/vector/src/main/res/layout/dialog_base_edit_text.xml @@ -11,7 +11,7 @@ android:paddingRight="?dialogPreferredPadding"> diff --git a/vector/src/main/res/layout/dialog_delete_event.xml b/vector/src/main/res/layout/dialog_confirmation_with_reason.xml similarity index 74% rename from vector/src/main/res/layout/dialog_delete_event.xml rename to vector/src/main/res/layout/dialog_confirmation_with_reason.xml index 08b0131f6a..859eda2010 100644 --- a/vector/src/main/res/layout/dialog_delete_event.xml +++ b/vector/src/main/res/layout/dialog_confirmation_with_reason.xml @@ -1,6 +1,7 @@ + app:layout_constraintTop_toBottomOf="@+id/dialogConfirmationText" /> + app:layout_constraintTop_toBottomOf="@+id/dialogReasonCheck"> diff --git a/vector/src/main/res/layout/dialog_edit_power_level.xml b/vector/src/main/res/layout/dialog_edit_power_level.xml new file mode 100644 index 0000000000..264563746d --- /dev/null +++ b/vector/src/main/res/layout/dialog_edit_power_level.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/dialog_no_sticker_pack.xml b/vector/src/main/res/layout/dialog_no_sticker_pack.xml new file mode 100644 index 0000000000..427916cd89 --- /dev/null +++ b/vector/src/main/res/layout/dialog_no_sticker_pack.xml @@ -0,0 +1,10 @@ + + diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 7c79451fbb..0c43c36911 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -108,22 +108,39 @@ app:layout_constraintTop_toBottomOf="@id/syncStateView" tools:listitem="@layout/item_timeline_event_base" /> - + app:layout_constraintTop_toBottomOf="@id/syncStateView"> + + + + + + @@ -150,6 +167,13 @@ app:layout_constraintTop_toBottomOf="@+id/roomToolbar" tools:visibility="visible" /> + + diff --git a/vector/src/main/res/layout/fragment_room_widget.xml b/vector/src/main/res/layout/fragment_room_widget.xml new file mode 100644 index 0000000000..4c1e5bb4a3 --- /dev/null +++ b/vector/src/main/res/layout/fragment_room_widget.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + \ No newline at end of file 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 index 964875bd08..06df565d8b 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_room_preview.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_room_preview.xml @@ -11,13 +11,13 @@ android:layout_width="48dp" android:layout_height="48dp" android:layout_marginStart="@dimen/layout_horizontal_margin" - android:layout_marginEnd="@dimen/layout_horizontal_margin" android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/layout_horizontal_margin" android:layout_marginBottom="8dp" android:adjustViewBounds="true" android:background="@drawable/circle" - android:importantForAccessibility="no" android:contentDescription="@string/avatar" + android:importantForAccessibility="no" android:scaleType="centerCrop" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -39,19 +39,33 @@ android:textSize="14sp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="@+id/bottomSheetRoomPreviewAvatar" - app:layout_constraintEnd_toStartOf="@+id/bottomSheetRoomPreviewSettings" + app:layout_constraintEnd_toStartOf="@+id/bottomSheetRoomPreviewFavorite" app:layout_constraintStart_toEndOf="@id/bottomSheetRoomPreviewAvatar" app:layout_constraintTop_toTopOf="@id/bottomSheetRoomPreviewAvatar" tools:text="@tools:sample/full_names" /> + + diff --git a/vector/src/main/res/layout/item_room_widget.xml b/vector/src/main/res/layout/item_room_widget.xml new file mode 100644 index 0000000000..8de7f244ee --- /dev/null +++ b/vector/src/main/res/layout/item_room_widget.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_notification_area.xml b/vector/src/main/res/layout/view_notification_area.xml index 8af520c2c7..b411a90ca7 100644 --- a/vector/src/main/res/layout/view_notification_area.xml +++ b/vector/src/main/res/layout/view_notification_area.xml @@ -4,14 +4,16 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:minHeight="42dp" - android:paddingTop="8dp" - android:paddingBottom="8dp" - tools:background="@color/vector_fuchsia_color" + android:minHeight="48dp" tools:parentTag="android.widget.RelativeLayout"> + + + tools:text="@string/room_do_not_have_permission_to_post" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/view_room_widgets_banner.xml b/vector/src/main/res/layout/view_room_widgets_banner.xml new file mode 100644 index 0000000000..125b4c9a22 --- /dev/null +++ b/vector/src/main/res/layout/view_room_widgets_banner.xml @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index 7eea0e2582..a4adb203f2 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -3,6 +3,11 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values-ar/strings.xml b/vector/src/main/res/values-ar/strings.xml index 35e8f246aa..b6ee3a329d 100644 --- a/vector/src/main/res/values-ar/strings.xml +++ b/vector/src/main/res/values-ar/strings.xml @@ -949,9 +949,6 @@ %1$s الآن %1$s منذ %2$s - أأعرض كل الرسائل من هذا المستخدم؟ - -سيُعيد هذا الإجراء تشغيل التطبيق وقد يأخذ بعض الوقت. "⁨%1$s⁩ و " ⁨%1$s⁩ و ⁨%2$s⁩ %1$s %2$s @@ -990,8 +987,6 @@ %d رسالة إخطار غير مقروءة %d رسالة إخطار غير مقروءة - أضاف ⁨%2$s⁩ المستخدم ⁨%1$s⁩ - أزال ⁨%2$s⁩ المستخدم ⁨%1$s⁩ استخدم مفتاح الإدخال في لوحة المفاتيح لإرسال الرسالة يمنع المستخدم حسب المعرّف المعطى يُلغي المنع عن المستخدم حسب المعرّف المعطى diff --git a/vector/src/main/res/values-bg/strings.xml b/vector/src/main/res/values-bg/strings.xml index 3428cbbfde..dda1f8eb8c 100644 --- a/vector/src/main/res/values-bg/strings.xml +++ b/vector/src/main/res/values-bg/strings.xml @@ -648,8 +648,6 @@ Трябва Ви разрешение, за да управлявате приспособленията в тази стая Създаване на приспособлението беше неуспешно - %1$s добавено от %2$s - %1$s премахнато от %2$s Създаване на групови разговори с jitsi Сигурни ли сте, че искате да изтриете приспособлението от тази стая? @@ -921,9 +919,6 @@ Тази стая е продължение от предишна кореспонденция Натиснете тук за да видите по-стари съобщения - Показване на всички съобщения от този потребител? - -Имайте предвид, че това действие ще рестартира приложението, което може да отнеме известно време. Поради липсващи разрешения, това действие не е възможно. 1 сек. @@ -1016,10 +1011,6 @@ Избор на мелодия за обаждания: Изгони - - Сигурни ли сте, че искате да изгоните този потребител от този чат? - Сигурни ли сте, че искате да изгоните тези потребители от този чат? - Причина Прегледи на връзки директно в самия чат, когато функцията се поддържа от сървъра. diff --git a/vector/src/main/res/values-bn-rIN/strings.xml b/vector/src/main/res/values-bn-rIN/strings.xml index ac67c0aec5..bd29fd0574 100644 --- a/vector/src/main/res/values-bn-rIN/strings.xml +++ b/vector/src/main/res/values-bn-rIN/strings.xml @@ -422,19 +422,12 @@ অ্যাডমিন কর এই ব্যবহারকারীর কাছ থেকে সব বার্তা লুকান এই ব্যবহারকারীর সব বার্তা দেখান - এই ব্যবহারকারীর কাছ থেকে সব বার্তা দেখান\? -\n -\nমনে রাখবেন যে এই পদক্ষেপটি অ্যাপ্লিকেশনটি পুনরায় চালু করবে এবং এটি কিছু সময় নিতে পারে। ব্যবহারকারী আইডি, নাম বা ইমেইল উল্লেখ সেশান তালিকা প্রদর্শন কর আপনি এই পরিবর্তনটি পূর্বাবস্থায় ফিরিয়ে আনতে সক্ষম হবেন না যেহেতু আপনি ব্যবহারকারীকে একই শক্তি স্তর হিসাবে প্রচার করার জন্য প্রচার করছেন। \nআপনি কি নিশ্চিত\? - - আপনি কি নিশ্চিত যে এই চ্যাট থেকে এই ব্যবহারকারী কে পদাঘাত করতে চান\? - আপনি কি নিশ্চিত যে এই চ্যাট থেকে এই ব্যবহারকারীদের কে পদাঘাত করতে চান\? - আপনি কি এই চ্যাট থেকে এই ব্যবহারকারীকে নিষিদ্ধ করতে চান\? কারণ @@ -1042,8 +1035,6 @@ আপনি এই রুমে উইজেট পরিচালনা করার অনুমতি প্রয়োজন উইজেট এর নির্মাণ ব্যর্থ হয়েছে - %1$s %2$s দ্বারা যোগ করা হয়েছে - %1$s %2$s দ্বারা সরানো হয়েছে Jitsi সঙ্গে কনফারেন্স কল তৈরি করুন আপনি কি এই রুমে উইজেট মুছে ফেলতে চান\? diff --git a/vector/src/main/res/values-ca/strings.xml b/vector/src/main/res/values-ca/strings.xml index 9be9a0c077..e482b81812 100644 --- a/vector/src/main/res/values-ca/strings.xml +++ b/vector/src/main/res/values-ca/strings.xml @@ -738,8 +738,6 @@ Atenció: es podria eliminar aquest fitxer si es desinstal·la l\'aplicació. No teniu permisos per a gestionar ginys en aquesta sala Ha fallat la creació del giny - %1$s afegit per %2$s - %1$s retirat per %2$s Fes conferències amb jitsi Confirmeu que voleu esborrar el giny d\'aquesta sala? @@ -920,9 +918,6 @@ En voleu afegir algun? Ara %1$s %1$s fa %2$s - Mostrar tots els missatges d\'aquest usuari? - -Tingueu en compte que aquesta acció reiniciarà l\'aplicació i pot trigar una estona. "%1$s,· " %1$s i %2$s %1$s %2$s @@ -1045,10 +1040,6 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i pot trigar una Escolliu el to per les trucades: Expulsar - - Esteu segur que voleu expulsar aquest usuari d\'aquest xat? - Esteu segur que voleu expulsar aquests usuaris d\'aquest xat? - Motiu Mostra la vista prèvia dels enllaços dins del xat en cas que el vostre servidor base suporti aquesta funcionalitat. diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml index 3722dca202..8f479ccbd8 100644 --- a/vector/src/main/res/values-cs/strings.xml +++ b/vector/src/main/res/values-cs/strings.xml @@ -409,11 +409,6 @@ Vaši e-mailovou adresu můžete přidat k profilu v nastavení. Zobrazit seznam relací Toto je náhled místnosti. Interakce s místností byla vypnuta. - - Opravdu chcete vyhodit tohoto uživatele z této konverzace\? - Opravdu chcete vyhodit tyto uživatele z této konverzace\? - Opravdu chcete vyhodit tyto uživatele z této konverzace\? - Opravdu chcete zakázat vstup tohoto uživatele do této konverzace\? Důvod @@ -502,10 +497,7 @@ Vaši e-mailovou adresu můžete přidat k profilu v nastavení. Pozvánka byla odeslána na %s, což není spárováno s tímto účtem. \nPřihlaste se s jiným účtem nebo přidejte tento e-mail ke svému současnému účtu. Snažíte se přistupovat k %s. Chcete vstoupit, abyste se mohli podílet na diskuzi\? - Zobrazit všechny zprávy od tohoto uživatele\? -\n -\nTato akce provede restart aplikace a může nějakou dobu trvat. - Tuto změnu nelze vrátit, protože povyšujete uživatele na stejnou úroveň, jakou máte vy. + Tuto změnu nelze vrátit, protože povyšujete uživatele na stejnou úroveň, jakou máte vy. \nOpravdu to chcete udělat\? Prosím zadejte jednu nebo více e-mailových adres nebo Matrix ID @@ -1145,8 +1137,6 @@ Vaši e-mailovou adresu můžete přidat k profilu v nastavení. Pro správu widgetů v této místnosti potřebujete oprávnění Založení widgetu se nezdařilo - %2$s přidal %1$s - %2$s odstranil(a) %1$s Založit konferenční hovor s jitsi Jste si jisti, že chcete smazat widget z této místnosti\? diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index c57319cce2..f1c77d2dbc 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -705,8 +705,6 @@ Du kannst sie jetzt aktivieren oder später über das Einstellungsmenü.Um Widgets in diesem Raum zu verwalten, ist eine Berechtigung erforderlich Widget konnte nicht erstellt werden - %1$s hinzugefügt von %2$s - %1$s entfernt von %2$s Konferenzgespräche mit jitsi durchführen Soll das Widget wirklich aus diesem Raum gelöscht werden? @@ -978,9 +976,6 @@ Du kannst sie jetzt aktivieren oder später über das Einstellungsmenü.Dieser Raum ist die Fortsetzung einer anderen Konversation Klicke hier um die älteren Nachrichten zu sehen - Zeige alle Nachrichten dieses Benutzers? - -Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen. Nicht berechtigt, diese Aktion durchzuführen. 1s @@ -1064,10 +1059,6 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.Trotzdem anrufen Kicken - - Bist du sicher, dass du diesen Benutzer aus diesem Chat kicken möchtest? - Bist du sicher, dass du diese Benutzer aus diesem Chat kicken möchtest? - Grund Im Chat Linkvorschau aktivieren, wenn dein Heimserver diese Funktion unterstützt. diff --git a/vector/src/main/res/values-eo/strings.xml b/vector/src/main/res/values-eo/strings.xml index 799cc686e0..c530d61ddd 100644 --- a/vector/src/main/res/values-eo/strings.xml +++ b/vector/src/main/res/values-eo/strings.xml @@ -565,9 +565,6 @@ Igi administranto Kaŝi ĉiujn mesaĝojn de ĉi tiu uzanto Remontri ĉiujn mesaĝojn de ĉi tiu uzanto - Ĉu remontri ĉiujn mesaĝojn de ĉi tiu uzanto\? -\n -\nRimarku, ke tiu ĉi ago reekigos la aplikaĵon, kio povas daŭri ioman temon. Identigilo de uzanto, nomo, aŭ retpoŝtadreso Mencii Montri liston de salutaĵoj diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index ec37ea2825..57a08046fe 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -735,8 +735,6 @@ Puedes hacerlo ahora o más tarde desde los ajustes de la aplicación. Necesitas permiso para gestionar los componentes en esta sala La creación del componente falló - %1$s añadido por %2$s - %1$s eliminado por %2$s Crear llamadas de conferencia con jitsi ¿Seguro que quieres eliminar el widget de esta sala\? @@ -963,9 +961,6 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu Escribe aquí… Si es posible, por favor escribe la descripción en inglés. - ¿Mostrar todos los mensajes de este usuario? - -Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de tiempo. Enviar una respuesta cifrada… Enviar una respuesta (sin cifrar)… Actualmente no eres miembro de ninguna comunidad. @@ -1076,10 +1071,6 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de Llamada de video en proceso… Expulsar - - ¿Seguro que quieres expulsar este usuario de esta conversación? - ¿Seguro que quieres expulsar estos usuarios de esta conversación? - Razón Diagnóstico de fallas diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml index faf596724d..2d8f425e74 100644 --- a/vector/src/main/res/values-eu/strings.xml +++ b/vector/src/main/res/values-eu/strings.xml @@ -659,8 +659,6 @@ Orain egin dezakezu edo gero aplikazioaren ezarpenetatik. Baimena behar duzu gela honetako trepetak kudeatzeko Trepetaren sorrerak huts egin du - %1$s gehitu du %2$s erabiltzaileak - %1$s kendu du %2$s erabiltzaileak Sortu konferentzia deiak Jitsi bidez Ziur trepeta ezabatu nahi duzula gela honetatik? @@ -900,9 +898,6 @@ Matrix-eko mezuen ikusgaitasuna e-mail sistemaren antekoa da. Guk zure mezuak ah Sartu zure pasahitza. Ahal dela idatzi deskripzioa ingelesez. - Erakutsi erabiltzaile honen mezu guztiak? - -Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat beharko lukeela. Zifratutako erantzuna bidalita… Bidali erantzuna (zifratu gabea)… Aurreikusi multimedia bidali aurretik @@ -1017,10 +1012,6 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar Hautatu deientzako doinua: Kanporatu - - Ziur erabiltzaile hau txat honetatik kanporatu nahi duzula? - Ziur erabiltzaile hauek txat honetatik kanporatu nahi dituzula? - Arrazoia Erakutsi txateko esteken aurrebista hasiera-zerbitzariak onartzen badu. diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 72addef9e4..23cc81310c 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -734,8 +734,6 @@ Tarvitse oikeudet pienoissovellusten hallintaan tässä huoneessa Pienoissovelluksen luonti epäonnistui - %2$s lisäsi %1$s:n - %2$s poisti %1$s:n Luo konferenssipuheluita jitsin avulla Haluatko varmasti poistaa pienoissovelluksen tästä huoneesta\? @@ -961,13 +959,6 @@ Haluatko lisätä paketteja? Oli %1$s %2$s sitten Poista huoneesta - Näytä tämän käyttäjän kaikki viestit\? -\n -\nHuomioi, että tämä toiminto käynnistää Riotin uudelleen ja siinä voi kestää jonkin aikaa. - - Oletko varma, että haluat poistaa tämän käyttäjän tästä keskustelusta\? - Oletko varma, että haluat poistaa nämä käyttäjät tästä keskustelusta\? - Syy "%1$s, " diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 33f834e870..a2e1a08fd6 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -669,8 +669,6 @@ Vous pouvez le faire maintenant ou plus tard à partir des paramètres de l’ap Vous avez besoin d’une permission pour gérer les widgets de ce salon La création du widget a échoué - %1$s ajouté par %2$s - %1$s supprimé par %2$s Créer des appels en téléconférence avec jitsi Voulez-vous vraiment supprimer le widget de ce salon ? @@ -908,9 +906,6 @@ Voulez-vous en ajouter ? Veuillez renseigner votre mot de passe. Si possible, veuillez écrire la description en anglais. - Afficher tous les messages de cet utilisateur ? - -Veuillez noter que cette action redémarrera l’application et pourra prendre un certain temps. Envoyer une réponse chiffrée… Envoyer une réponse (non chiffrée)… Prévisualiser le média avant de l’envoyer @@ -1020,10 +1015,6 @@ Veuillez noter que cette action redémarrera l’application et pourra prendre u Appeler quand même Expulser - - Voulez-vous vraiment expulser cet utilisateur de cette discussion ? - Voulez-vous vraiment expulser ces utilisateurs de cette discussion ? - Motif Afficher un aperçu des liens dans la discussion quand votre serveur d’accueil le permet. diff --git a/vector/src/main/res/values-gl/strings.xml b/vector/src/main/res/values-gl/strings.xml index c01d8afcf4..aa70225f69 100644 --- a/vector/src/main/res/values-gl/strings.xml +++ b/vector/src/main/res/values-gl/strings.xml @@ -456,8 +456,6 @@ Pode engadir a dirección de correo na sección de configuración de perfil.Enorme Fallou a creación do trebello - %1$s engadido por %2$s - %1$s eliminado por %2$s 1 trebello activo %d trebellos activos diff --git a/vector/src/main/res/values-hr/strings.xml b/vector/src/main/res/values-hr/strings.xml index def7f86139..fb155a01f3 100644 --- a/vector/src/main/res/values-hr/strings.xml +++ b/vector/src/main/res/values-hr/strings.xml @@ -577,16 +577,9 @@ Dodaj broj telefona Informacije o aplikaciji Prikaži informacije o aplikaciji u postavkama sustava. - Želite li prikazati sve poruke ovog korisnika\? -\nOva će radnja ponovno pokrenuti aplikaciju i to bi moglo potrajati. Nećete moći poništiti ovu izmjenu jer dotičnom korisniku dajete istu razinu upravljanja kao Vaša. \nJeste li sigurni\? - - Jeste li sigurni da želite izbaciti ovog korisnika iz razgovora\? - Jeste li sigurni da želite izbaciti ove korisnike iz razgovora\? - Jeste li sigurni da želite izbaciti ove korisnike iz razgovora\? - Jeste li sigurni da želite zabraniti korisniku pristup ovom razgovoru\? Jeste li sigurni da želite pozvati %s u ovoj razgovor\? Unesite jednu ili više adresa e-pošte ili identitet u Matrixu diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 077665fe64..021e5e5f3c 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -660,8 +660,6 @@ Ezt megteheted most vagy később az alkalmazás beállítások alatt. Engedélyre van szükséged a kisalkalmazások ebben a szobában való kezeléséhez Kisalkalmazás létrehozása sikertelen - %1$s hozzá lett adva %2$s által - %1$s eltávolításra került %2$s által Hozz létre konferencia hívást a jitsi segítségével Biztos vagy benne hogy törölni akarod ezt a kisalkalmazást ebből a szobából? @@ -903,9 +901,6 @@ Matrixban az üzenetek láthatósága hasonlít az e-mailre. Az üzenet törlés Kérlek add meg a jelszavad. Kérlek, ha lehetséges a leírást angolul írd. - Mutassuk ennek a felhasználónak minden üzenetét? - -Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe. Titkosított válasz küldése… Válasz küldése (titkosítás nélkül)… Média előnézete küldés előtt @@ -1015,10 +1010,6 @@ Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe.Hívás mindenképpen Elküld - - Biztos, hogy kirúgod ezt a felhasználót\? - Biztos, hogy kirúgod ezeket a felhasználókat\? - Ok URL előnézet a csevegő ablakban, ha a Matrix szervered támogatja ezt a lehetőséget. diff --git a/vector/src/main/res/values-id/strings.xml b/vector/src/main/res/values-id/strings.xml index 56f6d4deb2..6e020470b3 100644 --- a/vector/src/main/res/values-id/strings.xml +++ b/vector/src/main/res/values-id/strings.xml @@ -400,9 +400,6 @@ Anda mungkin ingin masuk dengan akun lain, atau tambahkan email ini ke akun Anda Jadikan admin Sembunyikan semua pesan dari pengguna ini Tunjukkan semua pesan dari pengguna ini - Tunjukkan semua pesan dari pengguna ini? - -Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu. ID Pengguna, Nama atau email Sebut Tunjukkan Daftar Perangkat @@ -578,8 +575,6 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema Anda butuh permisi untuk mengurus widget di ruang ini Pembuatan widget gagal - %1$s ditambahkan oleh %2$s - %1$s disingkirkan oleh %2$s Buat panggilan konferensi dengan jitsi Apa Anda yakin ingin menghapus widget tersebut dari ruang ini? @@ -967,9 +962,6 @@ Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu.Panggilan Video Sedang Berlangsung… Keluarkan - - Apakah Anda yakin ingin mengeluarkan pengguna-pengguna tersebut dari percakapan ini? - Alasan Versi %s diff --git a/vector/src/main/res/values-in/strings.xml b/vector/src/main/res/values-in/strings.xml index 56f6d4deb2..6e020470b3 100644 --- a/vector/src/main/res/values-in/strings.xml +++ b/vector/src/main/res/values-in/strings.xml @@ -400,9 +400,6 @@ Anda mungkin ingin masuk dengan akun lain, atau tambahkan email ini ke akun Anda Jadikan admin Sembunyikan semua pesan dari pengguna ini Tunjukkan semua pesan dari pengguna ini - Tunjukkan semua pesan dari pengguna ini? - -Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu. ID Pengguna, Nama atau email Sebut Tunjukkan Daftar Perangkat @@ -578,8 +575,6 @@ Perhatikan bahwa tindakan ini akan memulai ulang aplikasi dan mungkin cukup mema Anda butuh permisi untuk mengurus widget di ruang ini Pembuatan widget gagal - %1$s ditambahkan oleh %2$s - %1$s disingkirkan oleh %2$s Buat panggilan konferensi dengan jitsi Apa Anda yakin ingin menghapus widget tersebut dari ruang ini? @@ -967,9 +962,6 @@ Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu.Panggilan Video Sedang Berlangsung… Keluarkan - - Apakah Anda yakin ingin mengeluarkan pengguna-pengguna tersebut dari percakapan ini? - Alasan Versi %s diff --git a/vector/src/main/res/values-is/strings.xml b/vector/src/main/res/values-is/strings.xml index b4a9f59a46..c760ee128f 100644 --- a/vector/src/main/res/values-is/strings.xml +++ b/vector/src/main/res/values-is/strings.xml @@ -654,8 +654,6 @@ Til að halda áfram skaltu setja inn lykilorðið þitt. Þú þarft aðgangsheimildir til að sýsla með viðmótshluta á þessari spjallrás Gerð viðmótshluta mistókst - %1$s bætt við af %2$s - %1$s fjarlægt af %2$s Búa til símafundi með Jitsi Ertu viss um að þú viljir eyða viðmótshlutanum? Gat ekki búið til viðmótshluta. diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index bbb5220a7e..b75272d92a 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -712,8 +712,6 @@ Suono delle notifiche Mostra gli orari in formato 12 ore Devi avere il permesso per poter gestire i widget in questa stanza - %1$s aggiunto da %2$s - %1$s rimosso da %2$s Avvia una conferenza usando Jitsi Vuoi davvero eliminare questo widget dalla stanza\? @@ -985,9 +983,6 @@ Questa stanza contiene una conversazione cominciata altrove Clicca qui per vedere i messaggi precedenti - Mostrare tutti i messaggi di questo utente\? -\n -\nTieni presente che questa azione riavvierà l\'app e ciò potrebbe richiedere molto tempo. Non hai permessi sufficienti per effettuare questa azione. 1s @@ -1071,10 +1066,6 @@ Chiama comunque Butta fuori - - Sei sicuro di voler buttare fuori questo utente\? - Sei sicuro di voler buttare fuori questi utenti\? - Motivo Se il tuo Home Server supporta questa funzione, all\'interno delle chat verrà visualizzata un\'anteprima dei link postati. diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index 6507c31620..69d08a7524 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -543,10 +543,8 @@ Riotアプリがあなたの電話帳へアクセスすることを許可しま リクエストに room_id がありません。 リクエストの送信に失敗しました。 ウィジェットを作成できません。 - %1$s は %2$s によって削除されました この部屋のウィジェットを管理する権限が必要です ウィジェットの作成に失敗しました - %1$s は %2$s によって追加されました ウィジェットをこの部屋から削除してもよろしいですか? サーバーが利用できないか、オーバーロードしている可能性があります この部屋は検証されていない不明なデバイスが含まれています。 @@ -850,9 +848,6 @@ Riotアプリがあなたの電話帳へアクセスすることを許可しま 現在 %1$s %2$s 前 %1$s - このユーザからのすべてのメッセージを表示しますか? - -この操作によってアプリを再起動するため、しばらく時間がかかることにご注意ください。 "%1$s、 " %1$s と %2$s %1$s %2$s @@ -1001,9 +996,6 @@ Matrixでのメッセージの可視性は電子メールと同様です。メ 着信音を選んでください: 追い出す - - このチャットから本当にこのユーザーを追い出しますか? - 理由 サービスを初期化 diff --git a/vector/src/main/res/values-ko/strings.xml b/vector/src/main/res/values-ko/strings.xml index 14f7a082af..28d6800e41 100644 --- a/vector/src/main/res/values-ko/strings.xml +++ b/vector/src/main/res/values-ko/strings.xml @@ -420,18 +420,12 @@ 관리자로 하기 이 사용자의 모든 메시지 숨기기 이 사용자의 모든 메시지 보이기 - 이 사용자의 모든 메시지를 보이겠습니까\? -\n -\n이 동작은 앱을 재시작하며 일정 시간이 걸릴 수 있습니다. 사용자 ID, 이름 혹은 이메일 언급 기기 목록 보이기 사용자를 자신과 동일한 권한 등급으로 승격시키는 것은 취소할 수 없습니다. \n확신합니까\? - - 이 사용자들을 이 대화에서 추방하겠습니까\? - 이 사용자를 이 대화에서 출입 금지하겠습니까\? 이유 @@ -1010,8 +1004,6 @@ 이 방에서 위젯을 다루려면 권한이 있어야 합니다 위젯 생성 실패 - %1$s을(를) %2$s님이 추가함 - %1$s을(를) %2$s님이 삭제함 Jitsi로 회의 전화 만들기 이 방에서 위젯을 삭제하겠습니까\? diff --git a/vector/src/main/res/values-lv/strings.xml b/vector/src/main/res/values-lv/strings.xml index be08be4dd1..9b6a7b38fd 100644 --- a/vector/src/main/res/values-lv/strings.xml +++ b/vector/src/main/res/values-lv/strings.xml @@ -763,8 +763,6 @@ Nepazīstamas ierīces: Tev nepietiek atļauju, lai rīkotos ar vidžetiem šajā istabā Vidžeta izveide neizdevās - %1$s pievienoja %2$s - %1$s noņēma %2$s Konferences zvaniem izmantot Jitsi Vai tiešām vēlies dzēst vidžetu? diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml index 7d7e8e72e0..4ce368fca7 100644 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -725,8 +725,6 @@ U heeft toestemming nodig om widgets in dit gesprek te beheren Aanmaken van widget is mislukt - %1$s is toegevoegd door %2$s - %1$s is verwijderd door %2$s Vergadergesprekken maken met jitsi Weet u zeker dat u deze widget uit dit gesprek wilt verwijderen\? @@ -975,9 +973,6 @@ Enter-knop van toetsenbord gebruiken om berichten te versturen Toont een actie - Alle berichten van deze gebruiker tonen\? -\n -\nLet op: de app zal voor deze actie opnieuw opgestart worden; dit kan even duren. Verbant gebruiker met gegeven ID Heft verbanning van gebruiker met gegeven ID op Stel het machtsniveau van een gebruiker in @@ -1065,10 +1060,6 @@ Selecteer beltoon voor oproepen: Eruit sturen - - Weet u zeker dat u deze gebruiker uit dit gesprek wilt sturen\? - Weet u zeker dat u deze gebruikers uit dit gesprek wilt sturen\? - Reden Versie %s diff --git a/vector/src/main/res/values-nn/strings.xml b/vector/src/main/res/values-nn/strings.xml index aad1451984..f96d262e10 100644 --- a/vector/src/main/res/values-nn/strings.xml +++ b/vector/src/main/res/values-nn/strings.xml @@ -361,9 +361,6 @@ Gjer til administrator Gøym alle meldingar frå denne brukaren Syn alle meldingar frå denne brukaren - Syn alle meldingar frå denne brukaren\? -\n -\nMerk deg at denne handlinga startar programmet på nytt og kan ta litt tid. Brukar-ID, namn, eller e-post Nemn Vis sesjonsliste @@ -789,8 +786,6 @@ For å gå fram, ver venleg og skriv passordet ditt inn. Du trenger tillating til å handsama widgetar i dette rommet Widget-laging mislukkast - %1$s lagt til av %2$s - %1$s fjerna av %2$s Lag konferansesamtalar med Jitsi Er du sikker på at du vil slette widgeten frå dette rommet\? @@ -974,10 +969,6 @@ Meldingssynlegheit på Matrix liknar på epost. At vi gløymer meldingane dine t Ein videosamtale pågår… Spark - - Er du trygg på at du vil sparka denne brukaren frå samtala\? - Er du trygg på at du vil sparka desse brukarane frå samtala\? - Grunn Avanserte varslingsinnstillingar diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index 301ae3299e..be46232826 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -844,8 +844,6 @@ Czy chcesz dodać teraz kilka? Potrzebujesz uprawnień do zarządzania widżetami w tym pokoju Utworzenie widżetu nie powiodło się - %1$s został dodany przez %2$s - %1$s został usunięty przez %2$s Utwórz połączenia konferencyjne za pomocą jitsi Czy na pewno chcesz usunąć widżet z tego pokoju? Nie można utworzyć widżetu. @@ -963,8 +961,6 @@ Widoczność wiadomości w Matrix jest podobna do wiadomości e-mail. Nasze zapo - Pokazać wszystkie wiadomości od tego użytkownika? -Pamiętaj ta akcja może zresetować aplikacje i potrwać jakiś czas. 1 zaznaczone %d zaznaczone @@ -1037,11 +1033,6 @@ Pamiętaj ta akcja może zresetować aplikacje i potrwać jakiś czas. Zadzwoń mimo to Połączenia Wyrzuć - - Czy chcesz wyrzucić tego użytkownika z rozmowy\? - Czy chcesz wyrzucić tych użytkowników z rozmowy\? - - Formatowanie Markdown Pokaż zdarzenia dołączenia i wyjścia Dla komunikatów i błędów diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml index 9f2feb3062..72b124cfa0 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -760,8 +760,6 @@ Atenção: este arquivo poderá ser apagado se o aplicativo for desinstalado.Você necessita ter permissões para poder gerenciar os widgets desta sala A criação do widget falhou - %1$s adicionada/o por %2$s - %1$s removida/o por %2$s Criar chamadas de conferência com jitsi Você tem certeza que quer apagar este widget desta sala? @@ -932,9 +930,6 @@ Quer adicionar alguns agora? %1$s agora há %1$s %2$s - Exibir todas as mensagens desta(e) usuária(o)? - -Note que esta ação vai reiniciar o aplicativo e isso pode tomar um certo tempo. "%1$s, " %1$s e %2$s %1$s %2$s @@ -1072,10 +1067,6 @@ Por favor revise as configurações da conta. Configurações do Dispositivo. Notificações estão habilitadas para este dispositivo. Desconectar - - Você tem certeza que deseja desconectar este usuário deste chat? - Você tem certeza que deseja desconectar estes usuários deste chat? - Notificações não são permitidas para este dispositivo. Por favor revise as configurações do Riot. Habilitar diff --git a/vector/src/main/res/values-pt/strings.xml b/vector/src/main/res/values-pt/strings.xml index 56417b9a36..86ff1053d3 100755 --- a/vector/src/main/res/values-pt/strings.xml +++ b/vector/src/main/res/values-pt/strings.xml @@ -722,8 +722,6 @@ Dispositivos desconhecidos: Precisa de permissão para gerir os widgets nesta sala Criação de widget falhou - %1$s adicionado por %2$s - %1$s removido por %2$s Criar chamadas de conferência com o jitsi Tem a certeza que quer remover o widget? @@ -837,9 +835,6 @@ Adicionar alguns agora? %1$s agora há %1$s %2$s - Mostrar todas as mensagens deste utilizador? - -Note que esta acção reiniciará a aplicação e poderá demorar algum tempo. Tem a certeza que pretende banir este utilizador desta conversa? "%1$s, " diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index f937f0bc7e..1c1ecbef8c 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -708,8 +708,6 @@ Звук уведомлений Показывать метки времени в 12-часовом формате - %1$s добавлен %2$s - %1$s удален %2$s Вам нужно разрешение на управление виджетами в этой комнате Создание виджета не удалось @@ -987,9 +985,6 @@ Банит пользователя с указанным ID Разбанит пользователя с указанным ID Определение уровня доступа пользователя - Показать все сообщения от этого пользователя? - -Учтите, что это действие перезапустит приложение и может занять некоторое время. Сбросить уровень доступа для пользователя с данным ID Пригласить пользователя с данным ID в текущую комнату Покинуть комнату @@ -1098,11 +1093,6 @@ Идёт видеозвонок … Выгнать - - Вы уверены, что хотите выгнать этого пользователя из чата\? - Вы уверены, что хотите выгнать этих пользователей из чата\? - Вы уверены, что хотите выгнать этих пользователей из чата\? - Причина Поиск проблем с уведомлениями diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index 3ca6b4bf71..747c8a739e 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -667,8 +667,6 @@ Môžete to urobiť teraz, alebo to môžete urobiť neskôr v časti Nastavenia Musíte mať povolenie meniť widgety v tejto miestnosti Nie je možné vytvoriť widget - %2$s pridal widget %1$s - %2$s odstránil widget %1$s Konferenčné hovory uskutočňovať použitím jitsi Ste si istí, že chcete odstrániť widget z tejto miestnosti? @@ -950,9 +948,6 @@ Viditeľnosť správ odoslaných cez matrix funguje podobne ako viditeľnosť sp teraz %1$s %1$s pred %2$s - Zobraziť všetky správy od tohoto používateľa? - -Pozor! Vykonaním tejto akcie reštartujete aplikáciu a opätovné načítanie môže chvíľu trvať. "%1$s, " %1$s a %2$s %1$s %2$s @@ -1058,11 +1053,6 @@ Pozor! Vykonaním tejto akcie reštartujete aplikáciu a opätovné načítanie Prebiehajúci video hovor… Vykázať - - Ste si istí, že chcete z miestnosti vykázať tohto používateľa\? - Ste si istí, že chcete z miestnosti vykázať týchto používateľov\? - Ste si istí, že chcete z miestnosti vykázať týchto používateľov\? - Dôvod Riešiť problémy s oznámeniami diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index a7c62f4605..fe8b834be9 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -284,10 +284,6 @@ ID Përdoruesi, Emër ose email-i Përmendje Shfaq Listë Sesionesh - - Jeni i sigurt se doni të përzihet ky përdorues nga kjo fjalosje? - Jeni i sigurt se doni të përzihen këta përdorues nga kjo fjalosje? - Jeni i sigurt se doni të dëbohet ky përdorues nga kjo fjalosje? Arsye @@ -624,8 +620,6 @@ Të stërmëdha Krijimi i widget-it dështoi - %1$s u shtua nga %2$s - %1$s u hoq nga %2$s Krijoni thirrje konferencë me Jitsi-n Jeni i sigurt se doni të fshihet widget-i nga kjo dhomë? S’arrihet të krijohet widget-i. @@ -872,9 +866,6 @@ 1 anëtar %d anëtarë - Të shfaqen krejt mesazhet nga ky përdorues\? -\n -\nKini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë. S’do të jeni në gjendje ta zhbëni këtë ndryshim, ngaqë po e promovoni përdoruesin të ketë të njëjtën shkallë pushteti si ju vetë. \nJeni i sigurt\? diff --git a/vector/src/main/res/values-tr/strings.xml b/vector/src/main/res/values-tr/strings.xml index c4e2105154..772b3701cd 100644 --- a/vector/src/main/res/values-tr/strings.xml +++ b/vector/src/main/res/values-tr/strings.xml @@ -448,19 +448,12 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Yönetici yap Bu kullanıcının tüm mesajlarını gizle Bu kullanıcının tüm mesajlarını göster - Bu kullanıcının tüm mesajlarını göster -\n -\nBu işlem uygulamayı yeniden başlatak ve biraz zaman alacak. Kullanıcı ID, Ad ya da eposta Bahset Oturum Listesini Göster Bu işlemin geri dönüşü yok kullanıcıyı sen ile aynı erişim seviyesine getiriyorsun. \nEmin misin\? - - Kullanıcıyı bu sohbetten atmak istediğine emin misin\? - Kullanıcıları bu sohbetten atmak istediğine emin misin\? - Kullanıcıyı bu sohbetten engellemek istediğine emin misin\? Neden @@ -1017,8 +1010,6 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını Bu odada widgetları yönetebilmek için izine ihtiyacın var Widget oluşturma başarısız - %1$s %2$s tarafından eklendi - %1$s %2$s tarafından kaldırıldı Jitsi ile konferans çağrısı oluştur Bu widget\'ı odadan kaldırmak istediğine emin misin\? diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml index 4086598171..70a31dfa12 100755 --- a/vector/src/main/res/values-uk/strings.xml +++ b/vector/src/main/res/values-uk/strings.xml @@ -708,8 +708,6 @@ Для керування віджетами у цій кімнаті потрібен дозвіл Помилка створення віджету - %1$s додано %2$s - %1$s видалено %2$s Здійснювати конференц дзвінки через Jitsi Справді бажаєте видалити віджет? @@ -895,9 +893,6 @@ Зараз %1$s %1$s %2$s тому - Показати усі повідомлення від цього користувача? - -Зауважте, що це перезавантажить додаток та може зайняти деякий час. "%1$s, " %1$s та %2$s %1$s %2$s @@ -1090,12 +1085,6 @@ Все одно подзвонити Копнути - - Ви певні, що хочете викинути цього (%d) користувача з чату? - Ви певні, що хочете викинути цих користувачів з чату? - Ви певні, що хочете викинути цих користувачів з чату? - - Причина Попередній перегляд посилань у чаті, у разі якщо Ваш сервер підтримує таку можливість. diff --git a/vector/src/main/res/values-v21/theme_light.xml b/vector/src/main/res/values-v21/theme_light.xml index d5a31ad36f..8f52615316 100644 --- a/vector/src/main/res/values-v21/theme_light.xml +++ b/vector/src/main/res/values-v21/theme_light.xml @@ -2,8 +2,10 @@ diff --git a/vector/src/main/res/values-v23/theme_status.xml b/vector/src/main/res/values-v23/theme_status.xml index dac774e54c..9270c7fe0e 100644 --- a/vector/src/main/res/values-v23/theme_status.xml +++ b/vector/src/main/res/values-v23/theme_status.xml @@ -2,9 +2,10 @@ diff --git a/vector/src/main/res/values-v27/theme_status.xml b/vector/src/main/res/values-v27/theme_status.xml index 3c72ede37d..f29383f8a3 100644 --- a/vector/src/main/res/values-v27/theme_status.xml +++ b/vector/src/main/res/values-v27/theme_status.xml @@ -2,6 +2,7 @@ diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml index f460df6d24..600c08310e 100644 --- a/vector/src/main/res/values-zh-rCN/strings.xml +++ b/vector/src/main/res/values-zh-rCN/strings.xml @@ -662,8 +662,6 @@ 您需要权限来管理这个聊天室的小部件 创建小部件失败 - %1$s 被 %2$s 添加 - %1$s 被 %2$s 移除 用 jitsi 创建会议通话 您确定要删除这个小部件吗? @@ -896,9 +894,6 @@ Matrix 中的消息可见性类似于电子邮件。我们忘记您的消息意 发言 如果可能的话,请使用英文撰写问题描述。 - 显示来自该用户的所有信息? - -注意,此操作会重启应用并将花费一些时间。 发送加密的回复… 发送回复(未加密)… 发送前预览媒体文件 @@ -993,9 +988,6 @@ Matrix 中的消息可见性类似于电子邮件。我们忘记您的消息意 请选择来电铃声: 移除 - - 您确定要从此聊天室中移除这些用户吗? - 理由 版本 %s diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index 2853987daf..c9334c90db 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -784,8 +784,6 @@ 您需要相關權限以管理此聊天室的小部件 創建小部件失敗 - %1$s 已由 %2$s 新增 - %1$s 被 %2$s 移除 使用 jitsi 建立會議通話 您確定要從聊天室刪除小工具嗎? @@ -869,9 +867,6 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 請輸入您的密碼。 如果可以,請使用英文撰寫描述。 - 顯示所有從此使用者而來的訊息? - -注意此動作將會重新啟動應用程式,而其可能需要一點時間。 傳送加密回覆…… 傳送回覆(未加密)…… 在傳送前預覽媒體 @@ -964,9 +959,6 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意 無論如何都要通話 踢出 - - 您確定您想要從這個聊天中踢出這些使用者嗎? - 理由 當您的家伺服器支援此功能時在聊天中預覽連結。 diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml index 238fcbd6a2..9d6a092b49 100644 --- a/vector/src/main/res/values/colors_riotx.xml +++ b/vector/src/main/res/values/colors_riotx.xml @@ -175,6 +175,16 @@ #FF22262E #FF090A0C + + #EBEFF5 + #27303A + #27303A + + + #61708B + #E3E8F0 + #E3E8F0 + #FFF8E3 diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 35daa9f986..d559093cee 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -348,7 +348,7 @@ Calls Use default Riot ringtone for incoming calls Allow fallback call assist server - Will use "%s" as assist when your home server does not offers one (your IP address will be shared during a call) + Will use "%s" as assist when your home server does not offer one (your IP address will be shared during a call) Incoming call ringtone Select ringtone for calls: @@ -466,6 +466,7 @@ SESSIONS Invite + Cancel invite Leave this room Remove from this room Ban @@ -474,19 +475,35 @@ Reset to normal user Make moderator Make admin - Hide all messages from this user - Show all messages from this user - Show all messages from this user?\n\nNote that this action will restart the app and it may take some time. User ID, Name or email Mention Show Session List You will not be able to undo this change as you are promoting the user to have the same power level as yourself.\nAre you sure? - - Are you sure that you want to kick this user from this chat? - Are you sure that you want to kick these users from this chat? - - Are you sure that you want to ban this user from this chat? + Demote yourself? + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges." + Demote + + + Ignore user + Ignoring this user will remove their messages from rooms you share.\n\nYou can reverse this action at any time in the general settings. + Ignore + + Unignore user + Unignoring this user will show all messages from them again. + Unignore + + Cancel invite + Are you sure you want to cancel the invite for this user? + Kick user + Reason to kick + kicking user will remove them from this room.\n\nTo prevent them from joining again, you should ban them instead. + Ban user + Reason to ban + Unban user + Banning user will kick them from this room and prevent them from joining again. + Unbanning user will allow them to join the room again. + Reason You need permission to manage widgets in this room Widget creation has failed - %1$s added by %2$s - %1$s removed by %2$s Create conference calls with jitsi Are you sure you want to delete the widget from this room? 1 active widget %d active widgets + "VIEW" + "Active widgets" Widget @@ -1822,8 +1839,11 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming "Mentions only" "Mute" "Settings" + "Add to favorites" + "Remove from favorites" "Leave the room" "%1$s made no changes" + "You made no changes" Sends the given message as a spoiler Spoiler Type keywords to find a reaction. @@ -1834,7 +1854,9 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming %1$s made the room public to whoever knows the link. + You made the room public to whoever knows the link. %1$s made the room invite only. + You made the room invite only. Unread messages Liberate your communication @@ -2028,6 +2050,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Image. Audio File + Sticker Waiting… %s cancelled @@ -2065,6 +2088,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Security Learn more More + Admin Actions Room settings Notifications @@ -2083,6 +2107,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Admin in %1$s Moderator in %1$s + Default in %1$s Custom (%1$d) in %2$s Jump to read receipt @@ -2303,6 +2328,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming The encryption used by this room is not supported %s created and configured the room. + You created and configured the room. Almost there! Is the other device showing the same shield? Almost there! Waiting for confirmation… @@ -2426,5 +2452,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Alternatively, you can enter any other identity server URL Enter the URL of an identity server Submit + Set role diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index f3f84a36d8..33f04771c1 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -33,6 +33,8 @@ @color/riotx_touch_guard_bg_black @color/riotx_attachment_selector_background_black @color/riotx_attachment_selector_border_black + @color/riotx_room_active_widgets_banner_bg_black + @color/riotx_room_active_widgets_banner_text_black @drawable/highlighted_message_background_black diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index cf3ea8f474..777d626348 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -31,6 +31,8 @@ @color/riotx_touch_guard_bg_dark @color/riotx_attachment_selector_background_dark @color/riotx_attachment_selector_border_dark + @color/riotx_room_active_widgets_banner_bg_dark + @color/riotx_room_active_widgets_banner_text_dark @color/riotx_keys_backup_banner_accent_color_dark diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 4067c98935..0fd6afee08 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -32,6 +32,8 @@ @color/riotx_keys_backup_banner_accent_color_light @color/riotx_attachment_selector_background_light @color/riotx_attachment_selector_border_light + @color/riotx_room_active_widgets_banner_bg_light + @color/riotx_room_active_widgets_banner_text_light @drawable/highlighted_message_background_light diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index 15d43ddc34..846380465e 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -96,6 +96,26 @@ + + + + + + + + + +