diff --git a/CHANGES.md b/CHANGES.md index 422d084db6..f39242ed62 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ Changes in Element 1.0.12 (2020-XX-XX) Features ✨: - Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428) - Room setting: update join rules and guest access (#2442) + - Url preview (#481) - Store encrypted file in cache and cleanup decrypted file at each app start (#2512) - Emoji Keyboard (#2520) @@ -22,6 +23,7 @@ Translations 🗣: - SDK API changes ⚠️: + - RawCacheStrategy has been moved and renamed to CacheStrategy - FileService: remove useless FileService.DownloadMode Build 🧱: diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 606f57b467..eb8b8b9730 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -264,7 +264,7 @@ class KeysBackupTest : InstrumentedTest { assertNotNull(decryption) // - Check decryptKeyBackupData() returns stg val sessionData = keysBackup - .decryptKeyBackupData(keyBackupData!!, + .decryptKeyBackupData(keyBackupData, session.olmInboundGroupSession!!.sessionIdentifier(), cryptoTestData.roomId, decryption!!) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt index 944d1036d3..b6e5ae7364 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -111,7 +111,7 @@ class KeysBackupTestHelper( Assert.assertTrue(keysBackup.isEnabled) stateObserver.stopAndCheckStates(null) - return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!) + return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version) } /** diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt new file mode 100644 index 0000000000..9ee84fdfc6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType + +@RunWith(AndroidJUnit4::class) +internal class UrlsExtractorTest : InstrumentedTest { + + private val urlsExtractor = UrlsExtractor() + + @Test + fun wrongEventTypeTest() { + createEvent(body = "https://matrix.org") + .copy(type = EventType.STATE_ROOM_GUEST_ACCESS) + .let { urlsExtractor.extract(it) } + .size shouldBeEqualTo 0 + } + + @Test + fun oneUrlTest() { + createEvent(body = "https://matrix.org") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org" + } + } + + @Test + fun withoutProtocolTest() { + createEvent(body = "www.matrix.org") + .let { urlsExtractor.extract(it) } + .size shouldBeEqualTo 0 + } + + @Test + fun oneUrlWithParamTest() { + createEvent(body = "https://matrix.org?foo=bar") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org?foo=bar" + } + } + + @Test + fun oneUrlWithParamsTest() { + createEvent(body = "https://matrix.org?foo=bar&bar=foo") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org?foo=bar&bar=foo" + } + } + + @Test + fun oneUrlInlinedTest() { + createEvent(body = "Hello https://matrix.org, how are you?") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org" + } + } + + @Test + fun twoUrlsTest() { + createEvent(body = "https://matrix.org https://example.org") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 2 + result[0] shouldBeEqualTo "https://matrix.org" + result[1] shouldBeEqualTo "https://example.org" + } + } + + private fun createEvent(body: String): Event = Event( + type = EventType.MESSAGE, + content = MessageTextContent( + msgType = MessageType.MSGTYPE_TEXT, + body = body + ).toContent() + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt similarity index 83% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt index f4eada559e..2880d851d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt @@ -14,16 +14,16 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.raw +package org.matrix.android.sdk.api.cache -sealed class RawCacheStrategy { +sealed class CacheStrategy { // Data is always fetched from the server - object NoCache : RawCacheStrategy() + object NoCache : CacheStrategy() // Once data is retrieved, it is stored for the provided amount of time. // In case of error, and if strict is set to false, the cache can be returned if available - data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : RawCacheStrategy() + data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : CacheStrategy() // Once retrieved, the data is stored in cache and will be always get from the cache - object InfiniteCache : RawCacheStrategy() + object InfiniteCache : CacheStrategy() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt index 19549a338e..f1722b2189 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.raw +import org.matrix.android.sdk.api.cache.CacheStrategy + /** * Useful methods to fetch raw data from the server. The access token will not be used to fetched the data */ @@ -23,7 +25,7 @@ interface RawService { /** * Get a URL, either from cache or from the remote server, depending on the cache strategy */ - suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String + suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String /** * Specific case for the well-known file. Cache validity is 8 hours diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 56609610f1..8a95baf3cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.identity.IdentityService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -181,6 +182,11 @@ interface Session : */ fun widgetService(): WidgetService + /** + * Returns the media service associated with the session + */ + fun mediaService(): MediaService + /** * Returns the integration manager service associated with the session */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt new file mode 100644 index 0000000000..9040ec7d5c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.session.media + +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.JsonDict + +interface MediaService { + /** + * Extract URLs from an Event. + * @return the list of URLs contains in the body of the Event. It does not mean that URLs in this list have UrlPreview data + */ + fun extractUrls(event: Event): List + + /** + * Get Raw Url Preview data from the homeserver. There is no cache management for this request + * @param url The url to get the preview data from + * @param timestamp The optional timestamp + */ + suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict + + /** + * Get Url Preview data from the homeserver, or from cache, depending on the cache strategy + * @param url The url to get the preview data from + * @param timestamp The optional timestamp. Note that this parameter is not taken into account + * if the data is already in cache and the cache strategy allow to use it + * @param cacheStrategy the cache strategy, see the type for more details + */ + suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData + + /** + * Clear the cache of all retrieved UrlPreview data + */ + suspend fun clearCache() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt new file mode 100644 index 0000000000..33fc8b052b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.session.media + +/** + * Facility data class to get the common field of a PreviewUrl response form the server + * + * Example of return data for the url `https://matrix.org`: + *
+ * {
+ *     "matrix:image:size": 112805,
+ *     "og:description": "Matrix is an open standard for interoperable, decentralised, real-time communication",
+ *     "og:image": "mxc://matrix.org/2020-12-03_uFqjagCCTJbaaJxb",
+ *     "og:image:alt": "Matrix is an open standard for interoperable, decentralised, real-time communication",
+ *     "og:image:height": 467,
+ *     "og:image:type": "image/jpeg",
+ *     "og:image:width": 911,
+ *     "og:locale": "en_US",
+ *     "og:site_name": "Matrix.org",
+ *     "og:title": "Matrix.org",
+ *     "og:type": "website",
+ *     "og:url": "https://matrix.org"
+ * }
+ * 
+ */ +data class PreviewUrlData( + // Value of field "og:url". If not provided, this is the value passed in parameter + val url: String, + // Value of field "og:site_name" + val siteName: String?, + // Value of field "og:title" + val title: String?, + // Value of field "og:description" + val description: String?, + // Value of field "og:image" + val mxcUrl: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 973388da49..b970ec60e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -20,6 +20,7 @@ import io.realm.DynamicRealm import io.realm.RealmMigration import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import timber.log.Timber import javax.inject.Inject @@ -27,7 +28,7 @@ import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 5L + const val SESSION_STORE_SCHEMA_VERSION = 6L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -38,6 +39,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 4) migrateTo5(realm) + if (oldVersion <= 5) migrateTo6(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -89,4 +91,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.removeField("adminE2EByDefault") ?.removeField("preferredJitsiDomain") } + + private fun migrateTo6(realm: DynamicRealm) { + Timber.d("Step 5 -> 6") + realm.schema.create("PreviewUrlCacheEntity") + .addField(PreviewUrlCacheEntityFields.URL, String::class.java) + .setRequired(PreviewUrlCacheEntityFields.URL, true) + .addPrimaryKey(PreviewUrlCacheEntityFields.URL) + .addField(PreviewUrlCacheEntityFields.URL_FROM_SERVER, String::class.java) + .addField(PreviewUrlCacheEntityFields.SITE_NAME, String::class.java) + .addField(PreviewUrlCacheEntityFields.TITLE, String::class.java) + .addField(PreviewUrlCacheEntityFields.DESCRIPTION, String::class.java) + .addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java) + .addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt new file mode 100644 index 0000000000..b1e0b64405 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class PreviewUrlCacheEntity( + @PrimaryKey + var url: String = "", + + var urlFromServer: String? = null, + var siteName: String? = null, + var title: String? = null, + var description: String? = null, + var mxcUrl: String? = null, + + var lastUpdatedTimestamp: Long = 0L +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index f62312f8fc..bca2c42c9e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -48,6 +48,7 @@ import io.realm.annotations.RealmModule PushRulesEntity::class, PushRuleEntity::class, PushConditionEntity::class, + PreviewUrlCacheEntity::class, PusherEntity::class, PusherDataEntity::class, ReadReceiptsSummaryEntity::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt new file mode 100644 index 0000000000..a139c17439 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database.query + +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields + +/** + * Get the current PreviewUrlCacheEntity, return null if it does not exist + */ +internal fun PreviewUrlCacheEntity.Companion.get(realm: Realm, url: String): PreviewUrlCacheEntity? { + return realm.where() + .equalTo(PreviewUrlCacheEntityFields.URL, url) + .findFirst() +} + +/** + * Get the current PreviewUrlCacheEntity, create one if it does not exist + */ +internal fun PreviewUrlCacheEntity.Companion.getOrCreate(realm: Realm, url: String): PreviewUrlCacheEntity { + return get(realm, url) ?: realm.createObject(url) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultCleanRawCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/CleanRawCacheTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultCleanRawCacheTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/CleanRawCacheTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt index 3b0d7546e5..42b826de16 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.internal.raw -import org.matrix.android.sdk.api.raw.RawCacheStrategy +import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.raw.RawService import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -25,15 +25,15 @@ internal class DefaultRawService @Inject constructor( private val getUrlTask: GetUrlTask, private val cleanRawCacheTask: CleanRawCacheTask ) : RawService { - override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String { - return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy)) + override suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String { + return getUrlTask.execute(GetUrlTask.Params(url, cacheStrategy)) } override suspend fun getWellknown(userId: String): String { val homeServerDomain = userId.substringAfter(":") return getUrl( "https://$homeServerDomain/.well-known/matrix/client", - RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false) + CacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt index 1f4ca6d627..16633d90ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.raw import com.zhuinden.monarchy.Monarchy import okhttp3.ResponseBody -import org.matrix.android.sdk.api.raw.RawCacheStrategy +import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.internal.database.model.RawCacheEntity import org.matrix.android.sdk.internal.database.query.get import org.matrix.android.sdk.internal.database.query.getOrCreate @@ -32,7 +32,7 @@ import javax.inject.Inject internal interface GetUrlTask : Task { data class Params( val url: String, - val rawCacheStrategy: RawCacheStrategy + val cacheStrategy: CacheStrategy ) } @@ -42,14 +42,14 @@ internal class DefaultGetUrlTask @Inject constructor( ) : GetUrlTask { override suspend fun execute(params: GetUrlTask.Params): String { - return when (params.rawCacheStrategy) { - RawCacheStrategy.NoCache -> doRequest(params.url) - is RawCacheStrategy.TtlCache -> doRequestWithCache( + return when (params.cacheStrategy) { + CacheStrategy.NoCache -> doRequest(params.url) + is CacheStrategy.TtlCache -> doRequestWithCache( params.url, - params.rawCacheStrategy.validityDurationInMillis, - params.rawCacheStrategy.strict + params.cacheStrategy.validityDurationInMillis, + params.cacheStrategy.strict ) - RawCacheStrategy.InfiniteCache -> doRequestWithCache( + CacheStrategy.InfiniteCache -> doRequestWithCache( params.url, Long.MAX_VALUE, true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 25345e953c..c5f3f65a34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -102,6 +103,7 @@ internal class DefaultSession @Inject constructor( private val permalinkService: Lazy, private val secureStorageService: Lazy, private val profileService: Lazy, + private val mediaService: Lazy, private val widgetService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, @@ -263,6 +265,8 @@ internal class DefaultSession @Inject constructor( override fun widgetService(): WidgetService = widgetService.get() + override fun mediaService(): MediaService = mediaService.get() + override fun integrationManagerService() = integrationManagerService override fun callSignalingService(): CallSignalingService = callSignalingService.get() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index e6fd5a7a0c..659fcc8f5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.session.group.GroupModule import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule import org.matrix.android.sdk.internal.session.identity.IdentityModule import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule +import org.matrix.android.sdk.internal.session.media.MediaModule import org.matrix.android.sdk.internal.session.openid.OpenIdModule import org.matrix.android.sdk.internal.session.profile.ProfileModule import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker @@ -75,6 +76,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers GroupModule::class, ContentModule::class, CacheModule::class, + MediaModule::class, CryptoModule::class, PushersModule::class, OpenIdModule::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt index 39b6608de3..8242edac84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt @@ -22,19 +22,12 @@ import retrofit2.Call import retrofit2.http.GET internal interface CapabilitiesAPI { - /** * Request the homeserver capabilities */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities") fun getCapabilities(): Call - /** - * Request the upload capabilities - */ - @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") - fun getUploadCapabilities(): Call - /** * Request the versions */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt similarity index 89% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 8d289dfda5..f3686b02d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -29,6 +29,8 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor +import org.matrix.android.sdk.internal.session.media.GetMediaConfigResult +import org.matrix.android.sdk.internal.session.media.MediaAPI import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.wellknown.GetWellknownTask @@ -40,6 +42,7 @@ internal interface GetHomeServerCapabilitiesTask : Task internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( private val capabilitiesAPI: CapabilitiesAPI, + private val mediaAPI: MediaAPI, @SessionDatabase private val monarchy: Monarchy, private val eventBus: EventBus, private val getWellknownTask: GetWellknownTask, @@ -67,9 +70,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } }.getOrNull() - val uploadCapabilities = runCatching { - executeRequest(eventBus) { - apiCall = capabilitiesAPI.getUploadCapabilities() + val mediaConfig = runCatching { + executeRequest(eventBus) { + apiCall = mediaAPI.getMediaConfig() } }.getOrNull() @@ -83,11 +86,11 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig)) }.getOrNull() - insertInDb(capabilities, uploadCapabilities, versions, wellknownResult) + insertInDb(capabilities, mediaConfig, versions, wellknownResult) } private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, - getUploadCapabilitiesResult: GetUploadCapabilitiesResult?, + getMediaConfigResult: GetMediaConfigResult?, getVersionResult: Versions?, getWellknownResult: WellknownResult?) { monarchy.awaitTransaction { realm -> @@ -97,8 +100,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() } - if (getUploadCapabilitiesResult != null) { - homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize + if (getMediaConfigResult != null) { + homeServerCapabilitiesEntity.maxUploadFileSize = getMediaConfigResult.maxUploadSize ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt new file mode 100644 index 0000000000..004b622c64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import com.zhuinden.monarchy.Monarchy +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface ClearPreviewUrlCacheTask : Task + +internal class DefaultClearPreviewUrlCacheTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy +) : ClearPreviewUrlCacheTask { + + override suspend fun execute(params: Unit) { + monarchy.awaitTransaction { realm -> + realm.where() + .findAll() + .deleteAllFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt new file mode 100644 index 0000000000..1a400ccfcf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import androidx.collection.LruCache +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.util.getOrPut +import javax.inject.Inject + +internal class DefaultMediaService @Inject constructor( + private val clearPreviewUrlCacheTask: ClearPreviewUrlCacheTask, + private val getPreviewUrlTask: GetPreviewUrlTask, + private val getRawPreviewUrlTask: GetRawPreviewUrlTask, + private val urlsExtractor: UrlsExtractor +) : MediaService { + // Cache of extracted URLs + private val extractedUrlsCache = LruCache>(1_000) + + override fun extractUrls(event: Event): List { + return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) } + } + + private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}" + + override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict { + return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp)) + } + + override suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData { + return getPreviewUrlTask.execute(GetPreviewUrlTask.Params(url, timestamp, cacheStrategy)) + } + + override suspend fun clearCache() { + extractedUrlsCache.evictAll() + clearPreviewUrlCacheTask.execute(Unit) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt index 92903bf96e..fece6c06c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.homeserver +package org.matrix.android.sdk.internal.session.media import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class GetUploadCapabilitiesResult( +internal data class GetMediaConfigResult( /** * The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content. * If not listed or null, the size limit should be treated as unknown. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt new file mode 100644 index 0000000000..69cdfa8faa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import com.zhuinden.monarchy.Monarchy +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import java.util.Date +import javax.inject.Inject + +internal interface GetPreviewUrlTask : Task { + data class Params( + val url: String, + val timestamp: Long?, + val cacheStrategy: CacheStrategy + ) +} + +internal class DefaultGetPreviewUrlTask @Inject constructor( + private val mediaAPI: MediaAPI, + private val eventBus: EventBus, + @SessionDatabase private val monarchy: Monarchy +) : GetPreviewUrlTask { + + override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData { + return when (params.cacheStrategy) { + CacheStrategy.NoCache -> doRequest(params.url, params.timestamp) + is CacheStrategy.TtlCache -> doRequestWithCache( + params.url, + params.timestamp, + params.cacheStrategy.validityDurationInMillis, + params.cacheStrategy.strict + ) + CacheStrategy.InfiniteCache -> doRequestWithCache( + params.url, + params.timestamp, + Long.MAX_VALUE, + true + ) + } + } + + private suspend fun doRequest(url: String, timestamp: Long?): PreviewUrlData { + return executeRequest(eventBus) { + apiCall = mediaAPI.getPreviewUrlData(url, timestamp) + } + .toPreviewUrlData(url) + } + + private fun JsonDict.toPreviewUrlData(url: String): PreviewUrlData { + return PreviewUrlData( + url = (get("og:url") as? String) ?: url, + siteName = get("og:site_name") as? String, + title = get("og:title") as? String, + description = get("og:description") as? String, + mxcUrl = get("og:image") as? String + ) + } + + private suspend fun doRequestWithCache(url: String, timestamp: Long?, validityDurationInMillis: Long, strict: Boolean): PreviewUrlData { + // Get data from cache + var dataFromCache: PreviewUrlData? = null + var isCacheValid = false + monarchy.doWithRealm { realm -> + val entity = PreviewUrlCacheEntity.get(realm, url) + dataFromCache = entity?.toDomain() + isCacheValid = entity != null && Date().time < entity.lastUpdatedTimestamp + validityDurationInMillis + } + + val finalDataFromCache = dataFromCache + if (finalDataFromCache != null && isCacheValid) { + return finalDataFromCache + } + + // No cache or outdated cache + val data = try { + doRequest(url, timestamp) + } catch (throwable: Throwable) { + // In case of error, we can return value from cache even if outdated + return finalDataFromCache + ?.takeIf { !strict } + ?: throw throwable + } + + // Store cache + monarchy.awaitTransaction { realm -> + val previewUrlCacheEntity = PreviewUrlCacheEntity.getOrCreate(realm, url) + previewUrlCacheEntity.urlFromServer = data.url + previewUrlCacheEntity.siteName = data.siteName + previewUrlCacheEntity.title = data.title + previewUrlCacheEntity.description = data.description + previewUrlCacheEntity.mxcUrl = data.mxcUrl + + previewUrlCacheEntity.lastUpdatedTimestamp = Date().time + } + + return data + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt new file mode 100644 index 0000000000..6c5dad2422 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetRawPreviewUrlTask : Task { + data class Params( + val url: String, + val timestamp: Long? + ) +} + +internal class DefaultGetRawPreviewUrlTask @Inject constructor( + private val mediaAPI: MediaAPI, + private val eventBus: EventBus +) : GetRawPreviewUrlTask { + + override suspend fun execute(params: GetRawPreviewUrlTask.Params): JsonDict { + return executeRequest(eventBus) { + apiCall = mediaAPI.getPreviewUrlData(params.url, params.timestamp) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt new file mode 100644 index 0000000000..bbb4f1e06a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query + +internal interface MediaAPI { + /** + * Retrieve the configuration of the content repository + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-config + */ + @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") + fun getMediaConfig(): Call + + /** + * Get information about a URL for the client. Typically this is called when a client + * sees a URL in a message and wants to render a preview for the user. + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-preview-url + * @param url Required. The URL to get a preview of. + * @param ts The preferred point in time to return a preview for. The server may return a newer version + * if it does not have the requested version available. + */ + @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "preview_url") + fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt new file mode 100644 index 0000000000..bc58b3f444 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class MediaModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesMediaAPI(retrofit: Retrofit): MediaAPI { + return retrofit.create(MediaAPI::class.java) + } + } + + @Binds + abstract fun bindMediaService(service: DefaultMediaService): MediaService + + @Binds + abstract fun bindGetRawPreviewUrlTask(task: DefaultGetRawPreviewUrlTask): GetRawPreviewUrlTask + + @Binds + abstract fun bindGetPreviewUrlTask(task: DefaultGetPreviewUrlTask): GetPreviewUrlTask + + @Binds + abstract fun bindClearMediaCacheTask(task: DefaultClearPreviewUrlCacheTask): ClearPreviewUrlCacheTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt new file mode 100644 index 0000000000..dd1a9ead26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity + +/** + * PreviewUrlCacheEntity -> PreviewUrlData + */ +internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData( + url = urlFromServer ?: url, + siteName = siteName, + title = title, + description = description, + mxcUrl = mxcUrl +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt new file mode 100644 index 0000000000..9d374c3428 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.media + +import android.util.Patterns +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import javax.inject.Inject + +internal class UrlsExtractor @Inject constructor() { + // Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later + private val urlRegex = Patterns.WEB_URL.toRegex() + + fun extract(event: Event): List { + return event.takeIf { it.getClearType() == EventType.MESSAGE } + ?.getClearContent() + ?.toModel() + ?.takeIf { it.msgType == MessageType.MSGTYPE_TEXT || it.msgType == MessageType.MSGTYPE_EMOTE } + ?.body + ?.let { urlRegex.findAll(it) } + ?.map { it.value } + ?.filter { it.startsWith("https://") || it.startsWith("http://") } + ?.distinct() + ?.toList() + .orEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt new file mode 100644 index 0000000000..0998601db6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.util + +import androidx.collection.LruCache + +@Suppress("NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER") +internal inline fun LruCache.getOrPut(key: K, defaultValue: () -> V): V { + return get(key) ?: defaultValue().also { put(key, it) } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt index 169f24520b..3c48637e74 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt @@ -1,19 +1,17 @@ /* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.app.core.ui.views diff --git a/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt index a5e0005c2a..bb38150797 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt @@ -1,19 +1,17 @@ /* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.app.core.utils diff --git a/vector/src/main/java/im/vector/app/core/utils/Handler.kt b/vector/src/main/java/im/vector/app/core/utils/Handler.kt index c7ec97f53e..fe8760a522 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Handler.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Handler.kt @@ -1,19 +1,17 @@ /* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.app.core.utils diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 8891218a11..e034e373f3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -98,4 +98,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction() object QuickActionSetTopic : RoomDetailAction() data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction() + + // Preview URL + data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 3bf41a8c69..aff29bb7a3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -140,6 +140,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan @@ -1615,6 +1616,10 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(itemAction) } + override fun getPreviewUrlRetriever(): PreviewUrlRetriever { + return roomDetailViewModel.previewUrlRetriever + } + override fun onRoomCreateLinkClicked(url: String) { permalinkHandler .launch(requireContext(), url, object : NavigationInterceptor { @@ -1637,6 +1642,14 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } + override fun onPreviewUrlClicked(url: String) { + onUrlClicked(url, url) + } + + override fun onPreviewUrlCloseClicked(eventId: String, url: String) { + roomDetailViewModel.handle(RoomDetailAction.DoNotShowPreviewUrlFor(eventId, url)) + } + private fun onShareActionClicked(action: EventSharedAction.Share) { if (action.messageContent is MessageTextContent) { shareText(requireContext(), action.messageContent.body) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 13362c8a2e..14085a58a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -40,6 +40,7 @@ import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.raw.wellknown.getElementWellknown @@ -125,6 +126,9 @@ class RoomDetailViewModel @AssistedInject constructor( private var timelineEvents = PublishRelay.create>() val timeline = room.createTimeline(eventId, timelineSettings) + // Same lifecycle than the ViewModel (survive to screen rotation) + val previewUrlRetriever = PreviewUrlRetriever(session) + // Slot to keep a pending action during permission request var pendingAction: RoomDetailAction? = null @@ -277,9 +281,14 @@ class RoomDetailViewModel @AssistedInject constructor( RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) ) } + is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) }.exhaustive } + private fun handleDoNotShowPreviewUrlFor(action: RoomDetailAction.DoNotShowPreviewUrlFor) { + previewUrlRetriever.doNotShowPreviewUrlFor(action.eventId, action.url) + } + private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { viewModelScope.launch(Dispatchers.IO) { try { @@ -1336,6 +1345,17 @@ class RoomDetailViewModel @AssistedInject constructor( override fun onTimelineUpdated(snapshot: List) { timelineEvents.accept(snapshot) + + // PreviewUrl + if (vectorPreferences.showUrlPreviews()) { + withState { state -> + snapshot + .takeIf { state.asyncRoomSummary.invoke()?.isEncrypted == false } + ?.forEach { + previewUrlRetriever.getPreviewUrl(it.root, viewModelScope) + } + } + } } override fun onTimelineFailure(throwable: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index bddc7fa126..ba3ffe3174 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -48,6 +48,7 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.settings.VectorPreferences @@ -76,7 +77,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { - interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { + interface Callback : + BaseCallback, + ReactionPillCallback, + AvatarCallback, + UrlClickCallback, + ReadReceiptsCallback, + PreviewUrlCallback { fun onLoadMore(direction: Timeline.Direction) fun onEventInvisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent) @@ -91,6 +98,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // TODO move all callbacks to this? fun onTimelineItemAction(itemAction: RoomDetailAction) + + fun getPreviewUrlRetriever(): PreviewUrlRetriever } interface ReactionPillCallback { @@ -118,6 +127,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onUrlLongClicked(url: String): Boolean } + interface PreviewUrlCallback { + fun onPreviewUrlClicked(url: String) + fun onPreviewUrlCloseClicked(eventId: String, url: String) + } + // Map eventId to adapter position private val adapterPositionMapping = HashMap() private val modelCache = arrayListOf() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 2a98fd2dd7..27696f5b28 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -146,16 +146,16 @@ class MessageItemFactory @Inject constructor( // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) - is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) + is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) + is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } @@ -166,7 +166,7 @@ class MessageItemFactory @Inject constructor( callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { return when (messageContent.optionType) { - OPTION_TYPE_POLL -> { + OPTION_TYPE_POLL -> { MessagePollItem_() .attributes(attributes) .callback(callback) @@ -378,7 +378,7 @@ class MessageItemFactory @Inject constructor( val codeVisitor = CodeVisitor() codeVisitor.visit(localFormattedBody) when (codeVisitor.codeKind) { - CodeVisitor.Kind.BLOCK -> { + CodeVisitor.Kind.BLOCK -> { val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody) if (codeFormattedBlock == null) { buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) @@ -394,7 +394,7 @@ class MessageItemFactory @Inject constructor( buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) } } - CodeVisitor.Kind.NONE -> { + CodeVisitor.Kind.NONE -> { buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) } } @@ -431,6 +431,9 @@ class MessageItemFactory @Inject constructor( } .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .searchForPills(isFormatted) + .previewUrlRetriever(callback?.getPreviewUrlRetriever()) + .imageContentRenderer(imageContentRenderer) + .previewUrlCallback(callback) .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) @@ -536,6 +539,9 @@ class MessageItemFactory @Inject constructor( } } .leftGuideline(avatarSizeProvider.leftGuideline) + .previewUrlRetriever(callback?.getPreviewUrlRetriever()) + .imageContentRenderer(imageContentRenderer) + .previewUrlCallback(callback) .attributes(attributes) .highlighted(highlight) .movementMethod(createLinkMovementMethod(callback)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index f7a1a18d9f..8a8bf364e1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -1,19 +1,17 @@ /* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.app.features.home.room.detail.timeline.helper diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 3297f14622..c120fa671c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -1,19 +1,17 @@ /* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.app.features.home.room.detail.timeline.helper diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index feba62dea3..66d9808d2b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -23,7 +23,12 @@ import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R +import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlUiState +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView +import im.vector.app.features.media.ImageContentRenderer @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageTextItem : AbsMessageItem() { @@ -37,10 +42,27 @@ abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute var useBigFont: Boolean = false + @EpoxyAttribute + var previewUrlRetriever: PreviewUrlRetriever? = null + + @EpoxyAttribute + var previewUrlCallback: TimelineEventController.PreviewUrlCallback? = null + + @EpoxyAttribute + var imageContentRenderer: ImageContentRenderer? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var movementMethod: MovementMethod? = null + private val previewUrlViewUpdater = PreviewUrlViewUpdater() + override fun bind(holder: Holder) { + // Preview URL + previewUrlViewUpdater.previewUrlView = holder.previewUrlView + previewUrlViewUpdater.imageContentRenderer = imageContentRenderer + previewUrlRetriever?.addListener(attributes.informationData.eventId, previewUrlViewUpdater) + holder.previewUrlView.delegate = previewUrlCallback + if (useBigFont) { holder.messageView.textSize = 44F } else { @@ -65,12 +87,29 @@ abstract class MessageTextItem : AbsMessageItem() { holder.messageView.setTextFuture(textFuture) } + override fun unbind(holder: Holder) { + super.unbind(holder) + previewUrlViewUpdater.previewUrlView = null + previewUrlViewUpdater.imageContentRenderer = null + previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater) + } + override fun getViewType() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { val messageView by bind(R.id.messageTextView) + val previewUrlView by bind(R.id.messageUrlPreview) } + inner class PreviewUrlViewUpdater : PreviewUrlRetriever.PreviewUrlRetrieverListener { + var previewUrlView: PreviewUrlView? = null + var imageContentRenderer: ImageContentRenderer? = null + + override fun onStateUpdated(state: PreviewUrlUiState) { + val safeImageContentRenderer = imageContentRenderer ?: return + previewUrlView?.render(state, safeImageContentRenderer) + } + } companion object { private const val STUB_ID = R.id.messageContentTextStub } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt new file mode 100644 index 0000000000..695661feeb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt @@ -0,0 +1,127 @@ +/* + * 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.app.features.home.room.detail.timeline.url + +import im.vector.app.BuildConfig +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.Event + +class PreviewUrlRetriever(session: Session) { + private val mediaService = session.mediaService() + + private val data = mutableMapOf() + private val listeners = mutableMapOf>() + + // In memory list + private val blockedUrl = mutableSetOf() + + fun getPreviewUrl(event: Event, coroutineScope: CoroutineScope) { + val eventId = event.eventId ?: return + + synchronized(data) { + if (data[eventId] == null) { + // Keep only the first URL for the moment + val url = mediaService.extractUrls(event) + .firstOrNull() + ?.takeIf { it !in blockedUrl } + if (url == null) { + updateState(eventId, PreviewUrlUiState.NoUrl) + } else { + updateState(eventId, PreviewUrlUiState.Loading) + } + url + } else { + // Already handled + null + } + }?.let { urlToRetrieve -> + coroutineScope.launch { + runCatching { + mediaService.getPreviewUrl( + url = urlToRetrieve, + timestamp = null, + cacheStrategy = if (BuildConfig.DEBUG) CacheStrategy.NoCache else CacheStrategy.TtlCache(CACHE_VALIDITY, false) + ) + }.fold( + { + synchronized(data) { + // Blocked after the request has been sent? + if (urlToRetrieve in blockedUrl) { + updateState(eventId, PreviewUrlUiState.NoUrl) + } else { + updateState(eventId, PreviewUrlUiState.Data(eventId, urlToRetrieve, it)) + } + } + }, + { + synchronized(data) { + updateState(eventId, PreviewUrlUiState.Error(it)) + } + } + ) + } + } + } + + fun doNotShowPreviewUrlFor(eventId: String, url: String) { + blockedUrl.add(url) + + // Notify the listener + synchronized(data) { + data[eventId] + ?.takeIf { it is PreviewUrlUiState.Data && it.url == url } + ?.let { + updateState(eventId, PreviewUrlUiState.NoUrl) + } + } + } + + private fun updateState(eventId: String, state: PreviewUrlUiState) { + data[eventId] = state + // Notify the listener + listeners[eventId].orEmpty().forEach { + it.onStateUpdated(state) + } + } + + // Called by the Epoxy item during binding + fun addListener(key: String, listener: PreviewUrlRetrieverListener) { + listeners.getOrPut(key) { mutableSetOf() }.add(listener) + + // Give the current state if any + synchronized(data) { + listener.onStateUpdated(data[key] ?: PreviewUrlUiState.Unknown) + } + } + + // Called by the Epoxy item during unbinding + fun removeListener(key: String, listener: PreviewUrlRetrieverListener) { + listeners[key]?.remove(listener) + } + + interface PreviewUrlRetrieverListener { + fun onStateUpdated(state: PreviewUrlUiState) + } + + companion object { + // One week in millis + private const val CACHE_VALIDITY: Long = 7 * 24 * 3_600 * 1_000 + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt new file mode 100644 index 0000000000..a8f8f7b0cb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.url + +import org.matrix.android.sdk.api.session.media.PreviewUrlData + +/** + * The state representing a preview url UI state for an Event + */ +sealed class PreviewUrlUiState { + // No info + object Unknown : PreviewUrlUiState() + + // The event does not contain any URLs + object NoUrl : PreviewUrlUiState() + + // Loading + object Loading : PreviewUrlUiState() + + // Error + data class Error(val throwable: Throwable) : PreviewUrlUiState() + + // PreviewUrl data + data class Data(val eventId: String, + val url: String, + val previewUrlData: PreviewUrlData) : PreviewUrlUiState() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt new file mode 100755 index 0000000000..9d8f438683 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt @@ -0,0 +1,141 @@ +/* + * 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.app.features.home.room.detail.timeline.url + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.media.ImageContentRenderer +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.media.PreviewUrlData + +/** + * A View to display a PreviewUrl and some other state + */ +class PreviewUrlView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener { + + @BindView(R.id.url_preview_title) + lateinit var titleView: TextView + + @BindView(R.id.url_preview_image) + lateinit var imageView: ImageView + + @BindView(R.id.url_preview_description) + lateinit var descriptionView: TextView + + @BindView(R.id.url_preview_site) + lateinit var siteView: TextView + + @BindView(R.id.url_preview_close) + lateinit var closeView: View + + var delegate: TimelineEventController.PreviewUrlCallback? = null + + init { + setupView() + } + + private var state: PreviewUrlUiState = PreviewUrlUiState.Unknown + + /** + * This methods is responsible for rendering the view according to the newState + * + * @param newState the newState representing the view + */ + fun render(newState: PreviewUrlUiState, + imageContentRenderer: ImageContentRenderer, + force: Boolean = false) { + if (newState == state && !force) { + return + } + + state = newState + + hideAll() + when (newState) { + PreviewUrlUiState.Unknown, + PreviewUrlUiState.NoUrl -> renderHidden() + PreviewUrlUiState.Loading -> renderLoading() + is PreviewUrlUiState.Error -> renderHidden() + is PreviewUrlUiState.Data -> renderData(newState.previewUrlData, imageContentRenderer) + } + } + + override fun onClick(v: View?) { + when (val finalState = state) { + is PreviewUrlUiState.Data -> delegate?.onPreviewUrlClicked(finalState.url) + else -> Unit + } + } + + private fun onCloseClick() { + when (val finalState = state) { + is PreviewUrlUiState.Data -> delegate?.onPreviewUrlCloseClicked(finalState.eventId, finalState.url) + else -> Unit + } + } + + // PRIVATE METHODS **************************************************************************************************************************************** + + private fun setupView() { + inflate(context, R.layout.url_preview, this) + ButterKnife.bind(this) + + setOnClickListener(this) + closeView.setOnClickListener { onCloseClick() } + } + + private fun renderHidden() { + isVisible = false + } + + private fun renderLoading() { + // Just hide for the moment + isVisible = false + } + + private fun renderData(previewUrlData: PreviewUrlData, imageContentRenderer: ImageContentRenderer) { + isVisible = true + titleView.setTextOrHide(previewUrlData.title) + imageView.isVisible = previewUrlData.mxcUrl?.let { imageContentRenderer.render(it, imageView) }.orFalse() + descriptionView.setTextOrHide(previewUrlData.description) + siteView.setTextOrHide(previewUrlData.siteName.takeIf { it != previewUrlData.title }) + } + + /** + * Hide all views that are not visible in all state + */ + private fun hideAll() { + titleView.isVisible = false + imageView.isVisible = false + descriptionView.isVisible = false + siteView.isVisible = false + } +} diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index cf214b391a..4670c82db1 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -84,6 +84,19 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: STICKER } + /** + * For url preview + */ + fun render(mxcUrl: String, imageView: ImageView): Boolean { + val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() + val imageUrl = contentUrlResolver.resolveFullSize(mxcUrl) ?: return false + + GlideApp.with(imageView) + .load(imageUrl) + .into(imageView) + return true + } + /** * For gallery */ @@ -232,7 +245,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val resolvedUrl = when (mode) { Mode.FULL_SIZE, - Mode.STICKER -> resolveUrl(data) + Mode.STICKER -> resolveUrl(data) Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE) } // Fallback to base url @@ -300,7 +313,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: finalHeight = min(maxImageWidth * height / width, maxImageHeight) finalWidth = finalHeight * width / height } - Mode.STICKER -> { + Mode.STICKER -> { // limit on width val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2) finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 9d6ed0246c..c50692df82 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -783,6 +783,15 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_USE_ANALYTICS_KEY, false) } + /** + * Tells if the user wants to see URL previews in the timeline + * + * @return true if the user wants to see URL previews in the timeline + */ + fun showUrlPreviews(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SHOW_URL_PREVIEW_KEY, true) + } + /** * Enable or disable the analytics tracking. * diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index a84a10f74c..841a239701 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -22,7 +22,6 @@ import android.widget.CheckedTextView import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.preference.Preference -import androidx.preference.SwitchPreference import im.vector.app.R import im.vector.app.core.extensions.restart import im.vector.app.core.preference.VectorListPreference @@ -64,9 +63,9 @@ class VectorSettingsPreferencesFragment @Inject constructor( } // Url preview + /* + TODO Note: we keep the setting client side for now findPreference(VectorPreferences.SETTINGS_SHOW_URL_PREVIEW_KEY)!!.let { - /* - TODO it.isChecked = session.isURLPreviewEnabled it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> @@ -100,8 +99,8 @@ class VectorSettingsPreferencesFragment @Inject constructor( false } - */ } + */ // update keep medias period findPreference(VectorPreferences.SETTINGS_MEDIA_SAVING_PERIOD_KEY)!!.let { diff --git a/vector/src/main/res/drawable/ic_close_24dp.xml b/vector/src/main/res/drawable/ic_close_24dp.xml new file mode 100644 index 0000000000..d69c331210 --- /dev/null +++ b/vector/src/main/res/drawable/ic_close_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 15016e0abe..cfde244217 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -87,7 +87,6 @@ android:id="@+id/messageContentTextStub" style="@style/TimelineContentStubBaseParams" android:layout_height="wrap_content" - android:inflatedId="@id/messageTextView" android:layout="@layout/item_timeline_event_text_message_stub" tools:visibility="visible" /> diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml index 59396db0e5..7bdd0dd1e3 100644 --- a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml @@ -1,9 +1,26 @@ - + android:orientation="vertical"> + + + + + + diff --git a/vector/src/main/res/layout/url_preview.xml b/vector/src/main/res/layout/url_preview.xml new file mode 100644 index 0000000000..a8c287b471 --- /dev/null +++ b/vector/src/main/res/layout/url_preview.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml index 0412a03103..647ada65be 100644 --- a/vector/src/main/res/values/colors_riotx.xml +++ b/vector/src/main/res/values/colors_riotx.xml @@ -107,6 +107,13 @@ #FFA1B2D1 #FFA1B2D1 + + #FF8D99A5 + + #FF8D99A5 + + #FF8D99A5 + #FF61708B #FFA1B2D1 diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index 18ced0a071..ab0ecbe4e9 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -18,6 +18,7 @@ @color/riotx_header_panel_text_secondary_black @color/riotx_text_primary_black @color/riotx_text_secondary_black + @color/riotx_text_tertiary_black @color/riotx_text_primary_body_contrast_black @color/riotx_android_secondary_black @color/riotx_search_placeholder_black diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index cdd5cde488..6ebf8e2b9b 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -16,6 +16,7 @@ @color/riotx_header_panel_text_secondary_dark @color/riotx_text_primary_dark @color/riotx_text_secondary_dark + @color/riotx_text_tertiary_dark @color/riotx_text_primary_body_contrast_dark @color/riotx_android_secondary_dark @color/riotx_search_placeholder_dark diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 3c1505bb60..d7b91a37a7 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -16,6 +16,7 @@ @color/riotx_header_panel_text_secondary_light @color/riotx_text_primary_light @color/riotx_text_secondary_light + @color/riotx_text_tertiary_light @color/riotx_text_primary_body_contrast_light @color/riotx_android_secondary_light @color/riotx_search_placeholder_light diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index a162bf28fb..ad4cf8e3ed 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -57,8 +57,7 @@ android:defaultValue="true" android:key="SETTINGS_SHOW_URL_PREVIEW_KEY" android:summary="@string/settings_inline_url_preview_summary" - android:title="@string/settings_inline_url_preview" - app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_inline_url_preview" />