Merge branch 'develop' into feature/fix_1169

This commit is contained in:
Valere 2020-06-04 11:44:32 +02:00 committed by GitHub
commit 05efd7423e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
187 changed files with 6352 additions and 453 deletions

View File

@ -2,13 +2,16 @@ 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)
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)
Translations 🗣:
@ -21,7 +24,7 @@ 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
Changes in RiotX 0.21.0 (2020-05-28)
===================================================

View File

@ -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<Optional<Event>> {
fun liveStateEvent(eventType: String, stateKey: QueryStringValue): Observable<Optional<Event>> {
return room.getStateEventLive(eventType, stateKey).asObservable()
.startWithCallable {
room.getStateEvent(eventType, stateKey).toOptional()

View File

@ -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
@ -35,6 +36,7 @@ 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.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import im.vector.matrix.android.api.session.widgets.model.Widget
import io.reactivex.Observable
import io.reactivex.Single
@ -151,6 +153,18 @@ class RxSession(private val session: Session) {
session.getAccountDataEvents(types)
}
}
fun liveRoomWidgets(
roomId: String,
widgetId: QueryStringValue,
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): Observable<List<Widget>> {
return session.widgetService().getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes).asObservable()
.startWithCallable {
session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
}
}
}
fun Session.rx(): RxSession {

View File

@ -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", "<em>italic</em>")
}
/**
* 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, "<ul><li>item1</li></ul>") }
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li><li>item2</li></ul>") }
}
@Test
fun parseOrderedList() {
"1. item1".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li></ol>") }
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li><li>item2</li></ol>") }
}
@Test
fun parseHorizontalLine() {
"---".let { markdownParser.parse(it) }.expect("***", "<hr />")
}
@Test
fun parseH2AndContent() {
"a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "<h2>a</h2><p>b</p>")
}
@Test
fun parseQuote() {
"> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "<blockquote><p>quoted</p></blockquote>")
}
@Test
fun parseQuote_not_passing() {
"> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "<blockquote><p>quoted<br/>line2</p></blockquote>")
}
@Test
fun parseBoldItalic() {
"*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "<em>italic</em> <strong>bold</strong>")
"**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "<strong>bold</strong> <em>italic</em>")
}
@Test
fun parseHead() {
"# head1".let { markdownParser.parse(it) }.expect("head1", "<h1>head1</h1>")
"## head2".let { markdownParser.parse(it) }.expect("head2", "<h2>head2</h2>")
"### head3".let { markdownParser.parse(it) }.expect("head3", "<h3>head3</h3>")
"#### head4".let { markdownParser.parse(it) }.expect("head4", "<h4>head4</h4>")
"##### head5".let { markdownParser.parse(it) }.expect("head5", "<h5>head5</h5>")
"###### head6".let { markdownParser.parse(it) }.expect("head6", "<h6>head6</h6>")
}
@Test
fun parseHeads() {
"# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "<h1>head1</h1><h1>head2</h1>")
}
@Test
fun parseBoldNewLines_not_passing() {
"**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "<strong>bold</strong><br />line2")
}
@Test
fun parseLinks() {
"[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """<a href="target">link</a>""")
}
@Test
fun parseParagraph() {
"# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "<h1>head</h1><p>content</p>")
}
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</$htmlExpectedTag>")
// 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</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>")
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</$htmlExpectedTag>")
// With text before and space
"$textBefore $markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix",
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>")
// With sticked text after
"$markdownPattern$name$markdownPattern$textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
// With space and text after
"$markdownPattern$name$markdownPattern $textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $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</$htmlExpectedTag>$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</$htmlExpectedTag> $textAfter")
}
private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) {
assertEquals("TextContent are not identical", TextContent(expectedText, expectedFormattedText), this)
}
}

View File

@ -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);

View File

@ -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<String> = 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)

View File

@ -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
}

View File

@ -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<WellKnownManagerConfig> {
val managers = ArrayList<WellKnownManagerConfig>()
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
}
}
)

View File

@ -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,

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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
}
}

View File

@ -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<IntegrationManagerConfig>) {
// No-op
}
/**
* Is called whenever widget permissions from user account data are updated.
*/
fun onWidgetPermissionsChanged(widgets: Map<String, Boolean>) {
// 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<IntegrationManagerConfig>
/**
* 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<Unit>): 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<Unit>): 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<Unit>): 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
}

View File

@ -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
/**
@ -44,15 +43,15 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @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()) {
fun isAllowedToSend(isState: Boolean, eventType: String?, userId: String): Boolean {
return if (userId.isNotEmpty()) {
val powerLevel = getUserPowerLevel(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
}

View File

@ -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

View File

@ -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<Unit>)
fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable
fun getStateEvent(eventType: String, stateKey: String): Event?
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>>
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<Optional<Event>>
fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): List<Event>
fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>>
}

View File

@ -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
)

View File

@ -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()
}

View File

@ -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 <T> 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
}
}

View File

@ -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<String>? = null,
excludedTypes: Set<String>? = null
): List<Widget>
/**
* 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<String>? = null,
excludedTypes: Set<String>? = null
): LiveData<List<Widget>>
/**
* 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<String>? = null,
excludedTypes: Set<String>? = null
): List<Widget>
/**
* 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<String>? = null,
excludedTypes: Set<String>? = null
): LiveData<List<Widget>>
/**
* 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<Widget>): 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<Unit>): 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
}

View File

@ -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<String, String> = emptyMap(),
forceFetchScalarToken: Boolean = false,
bypassWhitelist: Boolean
): String
}

View File

@ -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()
}

View File

@ -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()
}
}

View File

@ -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<String> {
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)
}
}
}

View File

@ -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<Map<String, Any>>(JSON_DICT_PARAMETERIZED_TYPE)

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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,

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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<CurrentStateEventEntity> {
internal fun CurrentStateEventEntity.Companion.whereType(realm: Realm, roomId: String, type: String): RealmQuery<CurrentStateEventEntity> {
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<CurrentStateEventEntity> {
return where(realm = realm, roomId = roomId, type = type)
return whereType(realm = realm, roomId = roomId, type = type)
.equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey)
}

View File

@ -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<ScalarTokenEntity> {
return realm
.where<ScalarTokenEntity>()
.equalTo(ScalarTokenEntityFields.SERVER_URL, serverUrl)
}

View File

@ -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 <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
this.observe(owner, Observer { observer(it) })
}

View File

@ -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/"
}

View File

@ -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<OkHttpClient>, 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)

View File

@ -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<FileService>,
private val secureStorageService: Lazy<SecureStorageService>,
private val profileService: Lazy<ProfileService>,
private val widgetService: Lazy<WidgetService>,
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val syncTokenStore: SyncTokenStore,
@ -101,11 +105,13 @@ internal class DefaultSession @Inject constructor(
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val accountService: Lazy<AccountService>,
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)
}

View File

@ -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,

View File

@ -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 {

View File

@ -21,14 +21,16 @@ 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.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
}
}

View File

@ -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

View File

@ -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<String, Boolean> = 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<String, Map<String, Boolean>> = emptyMap()
)

View File

@ -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<IntegrationManagerConfig> {
return integrationManager.getOrderedConfigs()
}
override fun getPreferredConfig(): IntegrationManagerConfig {
return integrationManager.getPreferredConfig()
}
override fun isIntegrationEnabled(): Boolean {
return integrationManager.isIntegrationEnabled()
}
override fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): Cancelable {
return integrationManager.setIntegrationEnabled(enable, callback)
}
override fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): 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<Unit>): Cancelable {
return integrationManager.setNativeWidgetDomainAllowed(widgetType, domain, allowed, callback)
}
override fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean {
return integrationManager.isNativeWidgetDomainAllowed(widgetType, domain)
}
}

View File

@ -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<IntegrationManagerConfig>()
private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry }
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner)
private val listeners = HashSet<IntegrationManagerService.Listener>()
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<AllowedWidgetsContent>()
if (allowedWidgetsContent != null) {
notifyWidgetPermissionsChanged(allowedWidgetsContent)
}
}
accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING)
.observeNotNull(lifecycleOwner) {
val integrationProvisioningContent = it.getOrNull()?.content?.toModel<IntegrationProvisioningContent>()
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<IntegrationManagerConfig> {
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<IntegrationProvisioningContent>()
return integrationProvisioningContent?.enabled ?: false
}
fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): 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<Unit>): Cancelable {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
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<AllowedWidgetsContent>()
return currentContent?.widgets?.get(stateEventId) ?: false
}
fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
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<AllowedWidgetsContent>()
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<IntegrationManagerWidgetData>()
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()
}
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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>()
?: PowerLevelsContent()

View File

@ -116,9 +116,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
))

View File

@ -24,6 +24,7 @@ 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
@ -175,7 +176,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<String, String>): Call<Unit>
@Body params: JsonDict): Call<Unit>
/**
* Send a generic state events
@ -189,7 +190,7 @@ internal interface RoomAPI {
fun sendStateEvent(@Path("roomId") roomId: String,
@Path("state_event_type") stateEventType: String,
@Path("state_key") stateKey: String,
@Body params: Map<String, String>): Call<Unit>
@Body params: JsonDict): Call<Unit>
/**
* Send a relation event to a room.

View File

@ -66,6 +66,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 +82,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

View File

@ -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) }

View File

@ -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("<p>") && htmlText.endsWith("</p>\n")) {
htmlText.subStringBetween("<p>", "</p>\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 != "<p>${text.trim()}</p>\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 {

View File

@ -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("<p>") == 0) {
htmlText.removeSurrounding("<p>", "</p>\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 != "<p>${text.trim()}</p>\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", "")
}
}

View File

@ -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<Optional<Event>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm -> CurrentStateEventEntity.whereStateKey(realm, roomId, type = eventType, stateKey = "") },
{ it.root?.asDomain() }
override fun getStateEventLive(eventType: String, stateKey: QueryStringValue): LiveData<Optional<Event>> {
return stateEventDataSource.getStateEventLive(roomId, eventType, stateKey)
}
override fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> {
return stateEventDataSource.getStateEvents(roomId, eventTypes, stateKey)
}
override fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue): LiveData<List<Event>> {
return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey)
}
override fun sendStateEvent(
eventType: String,
stateKey: String?,
body: JsonDict,
callback: MatrixCallback<Unit>
): 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<Unit>) {
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<Unit>): Cancelable {
return sendStateEvent(
eventType = EventType.STATE_ROOM_TOPIC,
body = mapOf("topic" to topic),
callback = callback,
stateKey = null
)
}
}

View File

@ -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<SendStateTask.Params, Unit> {
data class Params(
val roomId: String,
val stateKey: String?,
val eventType: String,
val body: Map<String, String>
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
)
}
}
}
}

View File

@ -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<Optional<Event>> {
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<String>, stateKey: QueryStringValue): List<Event> {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
buildStateEventQuery(realm, roomId, eventTypes, stateKey)
.findAll()
.mapNotNull {
it.root?.asDomain()
}
}
}
fun getStateEventsLive(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): LiveData<List<Event>> {
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<String>,
stateKey: QueryStringValue
): RealmQuery<CurrentStateEventEntity> {
return realm.where<CurrentStateEventEntity>()
.equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
.`in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray())
.process(CurrentStateEventEntityFields.STATE_KEY, stateKey)
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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) {

View File

@ -241,7 +241,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

View File

@ -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"
}

View File

@ -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()

View File

@ -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<String, Any>
@Json(name = "content") val content: JsonDict
) : UserAccountData()

View File

@ -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()

View File

@ -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<String, Event>
) : UserAccountData()

View File

@ -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<GetTermsResponse>): 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<TermsResponse>(null) {
apiCall = termsAPI.getTerms("${url}terms")
}
GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData())
}
}
@ -76,13 +70,7 @@ internal class DefaultTermsService @Inject constructor(
token: String?,
callback: MatrixCallback<Unit>): 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<Unit>(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<String> {
return accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ACCEPTED_TERMS)
?.content

View File

@ -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<UserEntity> by lazy {
monarchy.createDataSourceFactory { realm ->
realm.where(UserEntity::class.java)
.isNotEmpty(UserEntityFields.USER_ID)
.sort(UserEntityFields.DISPLAY_NAME)
}
}
private val domainDataSourceFactory: DataSource.Factory<Int, User> by lazy {
realmDataSourceFactory.map {
it.asDomain()
}
}
private val livePagedListBuilder: LivePagedListBuilder<Int, User> 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<Optional<User>> {
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<List<User>> {
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<String>?): LiveData<PagedList<User>> {
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<List<User>> {
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<List<User>> {
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<String>, callback: MatrixCallback<Unit>): Cancelable {
val params = UpdateIgnoredUserIdsTask.Params(userIdsToIgnore = userIds.toList())
return updateIgnoredUserIdsTask

View File

@ -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<UserEntity> by lazy {
monarchy.createDataSourceFactory { realm ->
realm.where(UserEntity::class.java)
.isNotEmpty(UserEntityFields.USER_ID)
.sort(UserEntityFields.DISPLAY_NAME)
}
}
private val domainDataSourceFactory: DataSource.Factory<Int, User> by lazy {
realmDataSourceFactory.map {
it.asDomain()
}
}
private val livePagedListBuilder: LivePagedListBuilder<Int, User> 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<Optional<User>> {
val liveData = monarchy.findAllMappedWithChanges(
{ UserEntity.where(it, userId) },
{ it.asDomain() }
)
return Transformations.map(liveData) { results ->
results.firstOrNull().toOptional()
}
}
fun getUsersLive(): LiveData<List<User>> {
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<String>?): LiveData<PagedList<User>> {
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<List<User>> {
return monarchy.findAllMappedWithChanges(
{ realm ->
realm.where(IgnoredUserEntity::class.java)
.isNotEmpty(IgnoredUserEntityFields.USER_ID)
.sort(IgnoredUserEntityFields.USER_ID)
},
{ getUser(it.userId) ?: User(userId = it.userId) }
)
}
}

View File

@ -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) {

View File

@ -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<Map<String, Any>>(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<Optional<UserAccountDataEvent>> {
return Transformations.map(getLiveAccountDataEvents(setOf(type))) {
it.firstOrNull()?.toOptional()
}
return accountDataDataSource.getLiveAccountDataEvent(type)
}
override fun getAccountDataEvents(types: Set<String>): List<UserAccountDataEvent> {
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<String>): LiveData<List<UserAccountDataEvent>> {
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<Unit>?): Cancelable {

View File

@ -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<UpdateUserAccountDataTask.Pa
}
}
data class AllowedWidgets(override val type: String = UserAccountData.TYPE_ALLOWED_WIDGETS,
private val allowedWidgetsContent: AllowedWidgetsContent) : Params {
override fun getData(): Any {
return allowedWidgetsContent
}
}
data class IntegrationProvisioning(override val type: String = UserAccountData.TYPE_INTEGRATION_PROVISIONING,
private val integrationProvisioningContent: IntegrationProvisioningContent) : Params {
override fun getData(): Any {
return integrationProvisioningContent
}
}
data class AnyParams(override val type: String,
private val any: Any
) : Params {

View File

@ -0,0 +1,63 @@
/*
* 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.zhuinden.monarchy.Monarchy
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.internal.database.awaitNotEmptyResult
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields
import im.vector.matrix.android.internal.database.query.whereStateKey
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 CreateWidgetTask : Task<CreateWidgetTask.Params, Unit> {
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<Unit>(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)
}
}
}

View File

@ -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<JsonDict>(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 <T> sendObjectResponse(type: Type, response: T?, eventData: JsonDict) {
var jsString: String? = null
if (response != null) {
val objectAdapter = moshi.adapter<T>(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<String, Map<String, String>>()
val subMap = HashMap<String, String>()
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 ")
}
}
}

View File

@ -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<String>?,
excludedTypes: Set<String>?
): List<Widget> {
return widgetManager.getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
}
override fun getRoomWidgetsLive(
roomId: String,
widgetId: QueryStringValue,
widgetTypes: Set<String>?,
excludedTypes: Set<String>?
): LiveData<List<Widget>> {
return widgetManager.getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes)
}
override fun getUserWidgetsLive(
widgetTypes: Set<String>?,
excludedTypes: Set<String>?
): LiveData<List<Widget>> {
return widgetManager.getUserWidgetsLive(widgetTypes, excludedTypes)
}
override fun getUserWidgets(
widgetTypes: Set<String>?,
excludedTypes: Set<String>?
): List<Widget> {
return widgetManager.getUserWidgets(widgetTypes, excludedTypes)
}
override fun createRoomWidget(
roomId: String,
widgetId: String,
content: Content,
callback: MatrixCallback<Widget>
): Cancelable {
return widgetManager.createRoomWidget(roomId, widgetId, content, callback)
}
override fun destroyRoomWidget(
roomId: String,
widgetId: String,
callback: MatrixCallback<Unit>
): Cancelable {
return widgetManager.destroyRoomWidget(roomId, widgetId, callback)
}
override fun hasPermissionsToHandleWidgets(roomId: String): Boolean {
return widgetManager.hasPermissionsToHandleWidgets(roomId)
}
}

View File

@ -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<String> = emptyList()
fun start() {
setupWithConfiguration()
integrationManager.addListener(this)
}
fun stop() {
integrationManager.removeListener(this)
}
override fun onConfigurationChanged(configs: List<IntegrationManagerConfig>) {
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<String, String>, 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<String> = whiteListedUrls
for (allowedUrl in allowed) {
if (url.startsWith(allowedUrl)) {
return true
}
}
return false
}
private fun StringBuilder.appendParamsToUrl(params: Map<String, String>): 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
}
}

View File

@ -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?
)

View File

@ -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()
}
}

View File

@ -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<String>? = null,
excludedTypes: Set<String>? = null
): LiveData<List<Widget>> {
// 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<String>? = null,
excludedTypes: Set<String>? = null
): List<Widget> {
// Get all im.vector.modular.widgets state events in the room
val widgetEvents: List<Event> = 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<Event>.mapEventsToWidgets(widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null): List<Widget> {
val widgetEvents = this
// Widget id -> widget
val widgets: MutableMap<String, Widget> = 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<String>? = null,
excludedTypes: Set<String>? = null
): LiveData<List<Widget>> {
val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountData.TYPE_WIDGETS)
return Transformations.map(widgetsAccountData) {
it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList()
}
}
fun getUserWidgets(
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): List<Widget> {
val widgetsAccountData = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_WIDGETS) ?: return emptyList()
return widgetsAccountData.mapToWidgets(widgetTypes, excludedTypes)
}
private fun UserAccountDataEvent.mapToWidgets(widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null): List<Widget> {
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<Widget>): 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<Unit>): 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<PowerLevelsContent>() ?: return false
return PowerLevelsHelper(powerLevelsContent).isAllowedToSend(true, null, userId)
}
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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 requestOpenIdTokenResponse 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<RegisterWidgetResponse>
@GET("account")
fun validateToken(@Query("scalar_token") scalarToken: String?, @Query("v") version: String?): Call<Unit>
}

View File

@ -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<OkHttpClient>,
private val retrofitFactory: RetrofitFactory) {
// Map to keep one WidgetAPI instance by serverUrl
private val widgetsAPIs = mutableMapOf<String, WidgetsAPI>()
fun get(serverUrl: String): WidgetsAPI {
return widgetsAPIs.getOrPut(serverUrl) {
retrofitFactory.create(okHttpClient, serverUrl).create(WidgetsAPI::class.java)
}
}
}

View File

@ -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<Widget> {
return content.asSequence()
.mapNotNull {
@Suppress("UNCHECKED_CAST")
(it.value as? JsonDict)?.toModel<Event>()
}.mapNotNull { event ->
widgetFactory.create(event)
}
}

View File

@ -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<WidgetContent>()
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
}
}

View File

@ -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<GetScalarTokenTask.Params, String> {
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<RegisterWidgetResponse>(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<Unit>(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
}
}
}
}

View File

@ -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()
}
}
}

View File

@ -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 <T> CoroutineScope.launchToCallback(
val result = runCatching {
block()
}
result.foldToCallback(callback)
withContext(Dispatchers.Main) {
result.foldToCallback(callback)
}
}.toCancelable()

View File

@ -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<String> {
return resources.getStringArray(id)
}
}

View File

@ -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
}
}

View File

@ -82,6 +82,13 @@
<string name="notice_room_third_party_registered_invite">%1$s accepted the invitation for %2$s</string>
<string name="notice_room_third_party_registered_invite_by_you">You accepted the invitation for %1$s</string>
<string name="notice_widget_added">%1$s added %2$s widget</string>
<string name="notice_widget_added_by_you">You added %1$s widget</string>
<string name="notice_widget_removed">%1$s removed %2$s widget</string>
<string name="notice_widget_removed_by_you">You removed %1$s widget</string>
<string name="notice_widget_modified">%1$s modified %2$s widget</string>
<string name="notice_widget_modified_by_you">You modified %1$s widget</string>
<string name="notice_crypto_unable_to_decrypt">** Unable to decrypt: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">The sender\'s device has not sent us the keys for this message.</string>

View File

@ -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)

View File

@ -162,6 +162,7 @@
android:theme="@style/AppTheme.AttachmentsPreview" />
<activity android:name=".features.terms.ReviewTermsActivity" />
<activity android:name=".features.widgets.WidgetActivity" />
<!-- Services -->

View File

@ -102,6 +102,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 {
@ -510,4 +511,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(ReviewTermsFragment::class)
fun bindReviewTermsFragment(fragment: ReviewTermsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(WidgetFragment::class)
fun bindWidgetFragment(fragment: WidgetFragment): Fragment
}

View File

@ -36,6 +36,7 @@ import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceipt
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
@ -63,6 +64,8 @@ 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 = [
@ -120,6 +123,7 @@ interface ScreenComponent {
fun inject(activity: BigImageViewerActivity)
fun inject(activity: InviteUsersToRoomActivity)
fun inject(activity: ReviewTermsActivity)
fun inject(activity: WidgetActivity)
/* ==========================================================================================
* BottomSheets
@ -134,6 +138,8 @@ interface ScreenComponent {
fun inject(bottomSheet: DeviceVerificationInfoBottomSheet)
fun inject(bottomSheet: DeviceListBottomSheet)
fun inject(bottomSheet: BootstrapBottomSheet)
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
fun inject(bottomSheet: RoomWidgetsBottomSheet)
/* ==========================================================================================
* Others

View File

@ -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<ContentAttachmentData>, 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()
}

View File

@ -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
@ -65,6 +66,7 @@ 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 +74,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,6 +83,7 @@ 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
@ -131,6 +135,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 +148,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 +162,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 +202,8 @@ class RoomDetailFragment @Inject constructor(
VectorInviteView.Callback,
JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback {
AttachmentsHelper.Callback,
RoomWidgetsBannerView.Callback {
companion object {
@ -259,6 +268,8 @@ class RoomDetailFragment @Inject constructor(
setupNotificationView()
setupJumpToReadMarkerView()
setupJumpToBottomView()
setupWidgetsBannerView()
roomToolbarContentView.debouncedClicks {
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
}
@ -289,20 +300,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
@ -428,18 +468,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) {
@ -514,15 +560,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<MessageStickerContent>() ?: return
roomDetailViewModel.handle(RoomDetailAction.SendSticker(content))
}
}
}
}
@ -678,6 +728,7 @@ class RoomDetailFragment @Inject constructor(
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
@ -1381,7 +1432,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
}
@ -1414,4 +1465,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")
}
}

Some files were not shown because too many files have changed in this diff Show More