Merge pull request #2490 from vector-im/feature/bma/url_preview
Url preview
This commit is contained in:
commit
5203d15409
@ -4,6 +4,7 @@ Changes in Element 1.0.12 (2020-XX-XX)
|
|||||||
Features ✨:
|
Features ✨:
|
||||||
- Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428)
|
- Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428)
|
||||||
- Room setting: update join rules and guest access (#2442)
|
- 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)
|
- Store encrypted file in cache and cleanup decrypted file at each app start (#2512)
|
||||||
- Emoji Keyboard (#2520)
|
- Emoji Keyboard (#2520)
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ Translations 🗣:
|
|||||||
-
|
-
|
||||||
|
|
||||||
SDK API changes ⚠️:
|
SDK API changes ⚠️:
|
||||||
|
- RawCacheStrategy has been moved and renamed to CacheStrategy
|
||||||
- FileService: remove useless FileService.DownloadMode
|
- FileService: remove useless FileService.DownloadMode
|
||||||
|
|
||||||
Build 🧱:
|
Build 🧱:
|
||||||
|
@ -264,7 +264,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
assertNotNull(decryption)
|
assertNotNull(decryption)
|
||||||
// - Check decryptKeyBackupData() returns stg
|
// - Check decryptKeyBackupData() returns stg
|
||||||
val sessionData = keysBackup
|
val sessionData = keysBackup
|
||||||
.decryptKeyBackupData(keyBackupData!!,
|
.decryptKeyBackupData(keyBackupData,
|
||||||
session.olmInboundGroupSession!!.sessionIdentifier(),
|
session.olmInboundGroupSession!!.sessionIdentifier(),
|
||||||
cryptoTestData.roomId,
|
cryptoTestData.roomId,
|
||||||
decryption!!)
|
decryption!!)
|
||||||
|
@ -111,7 +111,7 @@ class KeysBackupTestHelper(
|
|||||||
Assert.assertTrue(keysBackup.isEnabled)
|
Assert.assertTrue(keysBackup.isEnabled)
|
||||||
|
|
||||||
stateObserver.stopAndCheckStates(null)
|
stateObserver.stopAndCheckStates(null)
|
||||||
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!)
|
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
@ -14,16 +14,16 @@
|
|||||||
* limitations under the License.
|
* 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
|
// 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.
|
// 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
|
// 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
|
// Once retrieved, the data is stored in cache and will be always get from the cache
|
||||||
object InfiniteCache : RawCacheStrategy()
|
object InfiniteCache : CacheStrategy()
|
||||||
}
|
}
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.raw
|
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
|
* 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
|
* 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
|
* Specific case for the well-known file. Cache validity is 8 hours
|
||||||
|
@ -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.homeserver.HomeServerCapabilitiesService
|
||||||
import org.matrix.android.sdk.api.session.identity.IdentityService
|
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.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.permalinks.PermalinkService
|
||||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||||
import org.matrix.android.sdk.api.session.pushers.PushersService
|
import org.matrix.android.sdk.api.session.pushers.PushersService
|
||||||
@ -181,6 +182,11 @@ interface Session :
|
|||||||
*/
|
*/
|
||||||
fun widgetService(): WidgetService
|
fun widgetService(): WidgetService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the media service associated with the session
|
||||||
|
*/
|
||||||
|
fun mediaService(): MediaService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the integration manager service associated with the session
|
* Returns the integration manager service associated with the session
|
||||||
*/
|
*/
|
||||||
|
@ -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<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
@ -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`:
|
||||||
|
* <pre>
|
||||||
|
* {
|
||||||
|
* "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"
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
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?
|
||||||
|
)
|
@ -20,6 +20,7 @@ import io.realm.DynamicRealm
|
|||||||
import io.realm.RealmMigration
|
import io.realm.RealmMigration
|
||||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
|
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.PendingThreePidEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -27,7 +28,7 @@ import javax.inject.Inject
|
|||||||
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
||||||
|
|
||||||
companion object {
|
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) {
|
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 <= 2) migrateTo3(realm)
|
||||||
if (oldVersion <= 3) migrateTo4(realm)
|
if (oldVersion <= 3) migrateTo4(realm)
|
||||||
if (oldVersion <= 4) migrateTo5(realm)
|
if (oldVersion <= 4) migrateTo5(realm)
|
||||||
|
if (oldVersion <= 5) migrateTo6(realm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo1(realm: DynamicRealm) {
|
private fun migrateTo1(realm: DynamicRealm) {
|
||||||
@ -89,4 +91,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||||||
?.removeField("adminE2EByDefault")
|
?.removeField("adminE2EByDefault")
|
||||||
?.removeField("preferredJitsiDomain")
|
?.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
@ -48,6 +48,7 @@ import io.realm.annotations.RealmModule
|
|||||||
PushRulesEntity::class,
|
PushRulesEntity::class,
|
||||||
PushRuleEntity::class,
|
PushRuleEntity::class,
|
||||||
PushConditionEntity::class,
|
PushConditionEntity::class,
|
||||||
|
PreviewUrlCacheEntity::class,
|
||||||
PusherEntity::class,
|
PusherEntity::class,
|
||||||
PusherDataEntity::class,
|
PusherDataEntity::class,
|
||||||
ReadReceiptsSummaryEntity::class,
|
ReadReceiptsSummaryEntity::class,
|
||||||
|
@ -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<PreviewUrlCacheEntity>()
|
||||||
|
.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)
|
||||||
|
}
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.raw
|
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 org.matrix.android.sdk.api.raw.RawService
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -25,15 +25,15 @@ internal class DefaultRawService @Inject constructor(
|
|||||||
private val getUrlTask: GetUrlTask,
|
private val getUrlTask: GetUrlTask,
|
||||||
private val cleanRawCacheTask: CleanRawCacheTask
|
private val cleanRawCacheTask: CleanRawCacheTask
|
||||||
) : RawService {
|
) : RawService {
|
||||||
override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String {
|
override suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String {
|
||||||
return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy))
|
return getUrlTask.execute(GetUrlTask.Params(url, cacheStrategy))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getWellknown(userId: String): String {
|
override suspend fun getWellknown(userId: String): String {
|
||||||
val homeServerDomain = userId.substringAfter(":")
|
val homeServerDomain = userId.substringAfter(":")
|
||||||
return getUrl(
|
return getUrl(
|
||||||
"https://$homeServerDomain/.well-known/matrix/client",
|
"https://$homeServerDomain/.well-known/matrix/client",
|
||||||
RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
|
CacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.raw
|
|||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import okhttp3.ResponseBody
|
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.model.RawCacheEntity
|
||||||
import org.matrix.android.sdk.internal.database.query.get
|
import org.matrix.android.sdk.internal.database.query.get
|
||||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
@ -32,7 +32,7 @@ import javax.inject.Inject
|
|||||||
internal interface GetUrlTask : Task<GetUrlTask.Params, String> {
|
internal interface GetUrlTask : Task<GetUrlTask.Params, String> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val url: String,
|
val url: String,
|
||||||
val rawCacheStrategy: RawCacheStrategy
|
val cacheStrategy: CacheStrategy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,14 +42,14 @@ internal class DefaultGetUrlTask @Inject constructor(
|
|||||||
) : GetUrlTask {
|
) : GetUrlTask {
|
||||||
|
|
||||||
override suspend fun execute(params: GetUrlTask.Params): String {
|
override suspend fun execute(params: GetUrlTask.Params): String {
|
||||||
return when (params.rawCacheStrategy) {
|
return when (params.cacheStrategy) {
|
||||||
RawCacheStrategy.NoCache -> doRequest(params.url)
|
CacheStrategy.NoCache -> doRequest(params.url)
|
||||||
is RawCacheStrategy.TtlCache -> doRequestWithCache(
|
is CacheStrategy.TtlCache -> doRequestWithCache(
|
||||||
params.url,
|
params.url,
|
||||||
params.rawCacheStrategy.validityDurationInMillis,
|
params.cacheStrategy.validityDurationInMillis,
|
||||||
params.rawCacheStrategy.strict
|
params.cacheStrategy.strict
|
||||||
)
|
)
|
||||||
RawCacheStrategy.InfiniteCache -> doRequestWithCache(
|
CacheStrategy.InfiniteCache -> doRequestWithCache(
|
||||||
params.url,
|
params.url,
|
||||||
Long.MAX_VALUE,
|
Long.MAX_VALUE,
|
||||||
true
|
true
|
@ -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.group.GroupService
|
||||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
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.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.permalinks.PermalinkService
|
||||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||||
import org.matrix.android.sdk.api.session.pushers.PushersService
|
import org.matrix.android.sdk.api.session.pushers.PushersService
|
||||||
@ -102,6 +103,7 @@ internal class DefaultSession @Inject constructor(
|
|||||||
private val permalinkService: Lazy<PermalinkService>,
|
private val permalinkService: Lazy<PermalinkService>,
|
||||||
private val secureStorageService: Lazy<SecureStorageService>,
|
private val secureStorageService: Lazy<SecureStorageService>,
|
||||||
private val profileService: Lazy<ProfileService>,
|
private val profileService: Lazy<ProfileService>,
|
||||||
|
private val mediaService: Lazy<MediaService>,
|
||||||
private val widgetService: Lazy<WidgetService>,
|
private val widgetService: Lazy<WidgetService>,
|
||||||
private val syncThreadProvider: Provider<SyncThread>,
|
private val syncThreadProvider: Provider<SyncThread>,
|
||||||
private val contentUrlResolver: ContentUrlResolver,
|
private val contentUrlResolver: ContentUrlResolver,
|
||||||
@ -263,6 +265,8 @@ internal class DefaultSession @Inject constructor(
|
|||||||
|
|
||||||
override fun widgetService(): WidgetService = widgetService.get()
|
override fun widgetService(): WidgetService = widgetService.get()
|
||||||
|
|
||||||
|
override fun mediaService(): MediaService = mediaService.get()
|
||||||
|
|
||||||
override fun integrationManagerService() = integrationManagerService
|
override fun integrationManagerService() = integrationManagerService
|
||||||
|
|
||||||
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
||||||
|
@ -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.homeserver.HomeServerCapabilitiesModule
|
||||||
import org.matrix.android.sdk.internal.session.identity.IdentityModule
|
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.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.openid.OpenIdModule
|
||||||
import org.matrix.android.sdk.internal.session.profile.ProfileModule
|
import org.matrix.android.sdk.internal.session.profile.ProfileModule
|
||||||
import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker
|
import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker
|
||||||
@ -75,6 +76,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
|||||||
GroupModule::class,
|
GroupModule::class,
|
||||||
ContentModule::class,
|
ContentModule::class,
|
||||||
CacheModule::class,
|
CacheModule::class,
|
||||||
|
MediaModule::class,
|
||||||
CryptoModule::class,
|
CryptoModule::class,
|
||||||
PushersModule::class,
|
PushersModule::class,
|
||||||
OpenIdModule::class,
|
OpenIdModule::class,
|
||||||
|
@ -22,19 +22,12 @@ import retrofit2.Call
|
|||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
|
||||||
internal interface CapabilitiesAPI {
|
internal interface CapabilitiesAPI {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the homeserver capabilities
|
* Request the homeserver capabilities
|
||||||
*/
|
*/
|
||||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities")
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities")
|
||||||
fun getCapabilities(): Call<GetCapabilitiesResult>
|
fun getCapabilities(): Call<GetCapabilitiesResult>
|
||||||
|
|
||||||
/**
|
|
||||||
* Request the upload capabilities
|
|
||||||
*/
|
|
||||||
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
|
|
||||||
fun getUploadCapabilities(): Call<GetUploadCapabilitiesResult>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the versions
|
* Request the versions
|
||||||
*/
|
*/
|
||||||
|
@ -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.di.UserId
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor
|
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.task.Task
|
||||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||||
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
|
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
|
||||||
@ -40,6 +42,7 @@ internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>
|
|||||||
|
|
||||||
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||||
private val capabilitiesAPI: CapabilitiesAPI,
|
private val capabilitiesAPI: CapabilitiesAPI,
|
||||||
|
private val mediaAPI: MediaAPI,
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
private val eventBus: EventBus,
|
private val eventBus: EventBus,
|
||||||
private val getWellknownTask: GetWellknownTask,
|
private val getWellknownTask: GetWellknownTask,
|
||||||
@ -67,9 +70,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
val uploadCapabilities = runCatching {
|
val mediaConfig = runCatching {
|
||||||
executeRequest<GetUploadCapabilitiesResult>(eventBus) {
|
executeRequest<GetMediaConfigResult>(eventBus) {
|
||||||
apiCall = capabilitiesAPI.getUploadCapabilities()
|
apiCall = mediaAPI.getMediaConfig()
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
@ -83,11 +86,11 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
|||||||
getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig))
|
getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig))
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
insertInDb(capabilities, uploadCapabilities, versions, wellknownResult)
|
insertInDb(capabilities, mediaConfig, versions, wellknownResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?,
|
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?,
|
||||||
getUploadCapabilitiesResult: GetUploadCapabilitiesResult?,
|
getMediaConfigResult: GetMediaConfigResult?,
|
||||||
getVersionResult: Versions?,
|
getVersionResult: Versions?,
|
||||||
getWellknownResult: WellknownResult?) {
|
getWellknownResult: WellknownResult?) {
|
||||||
monarchy.awaitTransaction { realm ->
|
monarchy.awaitTransaction { realm ->
|
||||||
@ -97,8 +100,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
|||||||
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
|
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getUploadCapabilitiesResult != null) {
|
if (getMediaConfigResult != null) {
|
||||||
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
|
homeServerCapabilitiesEntity.maxUploadFileSize = getMediaConfigResult.maxUploadSize
|
||||||
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
|
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
@ -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<Unit, Unit>
|
||||||
|
|
||||||
|
internal class DefaultClearPreviewUrlCacheTask @Inject constructor(
|
||||||
|
@SessionDatabase private val monarchy: Monarchy
|
||||||
|
) : ClearPreviewUrlCacheTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: Unit) {
|
||||||
|
monarchy.awaitTransaction { realm ->
|
||||||
|
realm.where<PreviewUrlCacheEntity>()
|
||||||
|
.findAll()
|
||||||
|
.deleteAllFromRealm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<String, List<String>>(1_000)
|
||||||
|
|
||||||
|
override fun extractUrls(event: Event): List<String> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@
|
|||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* 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
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -14,13 +14,13 @@
|
|||||||
* limitations under the License.
|
* 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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@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.
|
* 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.
|
* If not listed or null, the size limit should be treated as unknown.
|
@ -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<GetPreviewUrlTask.Params, PreviewUrlData> {
|
||||||
|
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<JsonDict>(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
|
||||||
|
}
|
||||||
|
}
|
@ -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<GetRawPreviewUrlTask.Params, JsonDict> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<GetMediaConfigResult>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<JsonDict>
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
@ -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<String> {
|
||||||
|
return event.takeIf { it.getClearType() == EventType.MESSAGE }
|
||||||
|
?.getClearContent()
|
||||||
|
?.toModel<MessageContent>()
|
||||||
|
?.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()
|
||||||
|
}
|
||||||
|
}
|
@ -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 <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
|
||||||
|
return get(key) ?: defaultValue().also { put(key, it) }
|
||||||
|
}
|
@ -1,19 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
* Copyright 2019 New Vector Ltd
|
*
|
||||||
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* 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
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
||||||
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.core.ui.views
|
package im.vector.app.core.ui.views
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
* Copyright 2019 New Vector Ltd
|
*
|
||||||
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* 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
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
||||||
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
package im.vector.app.core.utils
|
package im.vector.app.core.utils
|
||||||
|
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
* Copyright 2019 New Vector Ltd
|
*
|
||||||
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* 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
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
||||||
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.core.utils
|
package im.vector.app.core.utils
|
||||||
|
@ -98,4 +98,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||||||
data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction()
|
data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction()
|
||||||
object QuickActionSetTopic : RoomDetailAction()
|
object QuickActionSetTopic : RoomDetailAction()
|
||||||
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction()
|
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction()
|
||||||
|
|
||||||
|
// Preview URL
|
||||||
|
data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction()
|
||||||
}
|
}
|
||||||
|
@ -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.MessageTextItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
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.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.home.room.detail.widget.RoomWidgetsBottomSheet
|
||||||
import im.vector.app.features.html.EventHtmlRenderer
|
import im.vector.app.features.html.EventHtmlRenderer
|
||||||
import im.vector.app.features.html.PillImageSpan
|
import im.vector.app.features.html.PillImageSpan
|
||||||
@ -1615,6 +1616,10 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
roomDetailViewModel.handle(itemAction)
|
roomDetailViewModel.handle(itemAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getPreviewUrlRetriever(): PreviewUrlRetriever {
|
||||||
|
return roomDetailViewModel.previewUrlRetriever
|
||||||
|
}
|
||||||
|
|
||||||
override fun onRoomCreateLinkClicked(url: String) {
|
override fun onRoomCreateLinkClicked(url: String) {
|
||||||
permalinkHandler
|
permalinkHandler
|
||||||
.launch(requireContext(), url, object : NavigationInterceptor {
|
.launch(requireContext(), url, object : NavigationInterceptor {
|
||||||
@ -1637,6 +1642,14 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
|
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) {
|
private fun onShareActionClicked(action: EventSharedAction.Share) {
|
||||||
if (action.messageContent is MessageTextContent) {
|
if (action.messageContent is MessageTextContent) {
|
||||||
shareText(requireContext(), action.messageContent.body)
|
shareText(requireContext(), action.messageContent.body)
|
||||||
|
@ -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.sticker.StickerPickerActionHandler
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
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.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.home.room.typing.TypingHelper
|
||||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||||
import im.vector.app.features.raw.wellknown.getElementWellknown
|
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||||
@ -125,6 +126,9 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
|
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
|
||||||
val timeline = room.createTimeline(eventId, timelineSettings)
|
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
|
// Slot to keep a pending action during permission request
|
||||||
var pendingAction: RoomDetailAction? = null
|
var pendingAction: RoomDetailAction? = null
|
||||||
|
|
||||||
@ -277,9 +281,14 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
|
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleDoNotShowPreviewUrlFor(action: RoomDetailAction.DoNotShowPreviewUrlFor) {
|
||||||
|
previewUrlRetriever.doNotShowPreviewUrlFor(action.eventId, action.url)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
|
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@ -1336,6 +1345,17 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||||
timelineEvents.accept(snapshot)
|
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) {
|
override fun onTimelineFailure(throwable: Throwable) {
|
||||||
|
@ -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.MessageInformationData
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
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.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.ImageContentRenderer
|
||||||
import im.vector.app.features.media.VideoContentRenderer
|
import im.vector.app.features.media.VideoContentRenderer
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
@ -76,7 +77,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
private val backgroundHandler: Handler
|
private val backgroundHandler: Handler
|
||||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
) : 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 onLoadMore(direction: Timeline.Direction)
|
||||||
fun onEventInvisible(event: TimelineEvent)
|
fun onEventInvisible(event: TimelineEvent)
|
||||||
fun onEventVisible(event: TimelineEvent)
|
fun onEventVisible(event: TimelineEvent)
|
||||||
@ -91,6 +98,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
|
|
||||||
// TODO move all callbacks to this?
|
// TODO move all callbacks to this?
|
||||||
fun onTimelineItemAction(itemAction: RoomDetailAction)
|
fun onTimelineItemAction(itemAction: RoomDetailAction)
|
||||||
|
|
||||||
|
fun getPreviewUrlRetriever(): PreviewUrlRetriever
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReactionPillCallback {
|
interface ReactionPillCallback {
|
||||||
@ -118,6 +127,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
fun onUrlLongClicked(url: String): Boolean
|
fun onUrlLongClicked(url: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PreviewUrlCallback {
|
||||||
|
fun onPreviewUrlClicked(url: String)
|
||||||
|
fun onPreviewUrlCloseClicked(eventId: String, url: String)
|
||||||
|
}
|
||||||
|
|
||||||
// Map eventId to adapter position
|
// Map eventId to adapter position
|
||||||
private val adapterPositionMapping = HashMap<String, Int>()
|
private val adapterPositionMapping = HashMap<String, Int>()
|
||||||
private val modelCache = arrayListOf<CacheItemData?>()
|
private val modelCache = arrayListOf<CacheItemData?>()
|
||||||
|
@ -146,16 +146,16 @@ class MessageItemFactory @Inject constructor(
|
|||||||
// val all = event.root.toContent()
|
// val all = event.root.toContent()
|
||||||
// val ev = all.toModel<Event>()
|
// val ev = all.toModel<Event>()
|
||||||
return when (messageContent) {
|
return when (messageContent) {
|
||||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
|
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
|
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
|
||||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
|
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
|
||||||
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
||||||
return when (messageContent.optionType) {
|
return when (messageContent.optionType) {
|
||||||
OPTION_TYPE_POLL -> {
|
OPTION_TYPE_POLL -> {
|
||||||
MessagePollItem_()
|
MessagePollItem_()
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
@ -378,7 +378,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
val codeVisitor = CodeVisitor()
|
val codeVisitor = CodeVisitor()
|
||||||
codeVisitor.visit(localFormattedBody)
|
codeVisitor.visit(localFormattedBody)
|
||||||
when (codeVisitor.codeKind) {
|
when (codeVisitor.codeKind) {
|
||||||
CodeVisitor.Kind.BLOCK -> {
|
CodeVisitor.Kind.BLOCK -> {
|
||||||
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
|
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
|
||||||
if (codeFormattedBlock == null) {
|
if (codeFormattedBlock == null) {
|
||||||
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
@ -394,7 +394,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
|
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CodeVisitor.Kind.NONE -> {
|
CodeVisitor.Kind.NONE -> {
|
||||||
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
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()))
|
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
|
||||||
.searchForPills(isFormatted)
|
.searchForPills(isFormatted)
|
||||||
|
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
|
||||||
|
.imageContentRenderer(imageContentRenderer)
|
||||||
|
.previewUrlCallback(callback)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
@ -536,6 +539,9 @@ class MessageItemFactory @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
.previewUrlRetriever(callback?.getPreviewUrlRetriever())
|
||||||
|
.imageContentRenderer(imageContentRenderer)
|
||||||
|
.previewUrlCallback(callback)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.movementMethod(createLinkMovementMethod(callback))
|
.movementMethod(createLinkMovementMethod(callback))
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
* Copyright 2019 New Vector Ltd
|
*
|
||||||
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* 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
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
||||||
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.helper
|
package im.vector.app.features.home.room.detail.timeline.helper
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
* Copyright 2019 New Vector Ltd
|
*
|
||||||
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
||||||
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
||||||
* 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
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
||||||
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
package im.vector.app.features.home.room.detail.timeline.helper
|
package im.vector.app.features.home.room.detail.timeline.helper
|
||||||
|
|
||||||
|
@ -23,7 +23,12 @@ import androidx.core.widget.TextViewCompat
|
|||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
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.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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||||
@ -37,10 +42,27 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var useBigFont: Boolean = false
|
var useBigFont: Boolean = false
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var previewUrlRetriever: PreviewUrlRetriever? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var previewUrlCallback: TimelineEventController.PreviewUrlCallback? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var imageContentRenderer: ImageContentRenderer? = null
|
||||||
|
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||||
var movementMethod: MovementMethod? = null
|
var movementMethod: MovementMethod? = null
|
||||||
|
|
||||||
|
private val previewUrlViewUpdater = PreviewUrlViewUpdater()
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
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) {
|
if (useBigFont) {
|
||||||
holder.messageView.textSize = 44F
|
holder.messageView.textSize = 44F
|
||||||
} else {
|
} else {
|
||||||
@ -65,12 +87,29 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||||||
holder.messageView.setTextFuture(textFuture)
|
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
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||||
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
||||||
|
val previewUrlView by bind<PreviewUrlView>(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 {
|
companion object {
|
||||||
private const val STUB_ID = R.id.messageContentTextStub
|
private const val STUB_ID = R.id.messageContentTextStub
|
||||||
}
|
}
|
||||||
|
@ -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<String, PreviewUrlUiState>()
|
||||||
|
private val listeners = mutableMapOf<String, MutableSet<PreviewUrlRetrieverListener>>()
|
||||||
|
|
||||||
|
// In memory list
|
||||||
|
private val blockedUrl = mutableSetOf<String>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -84,6 +84,19 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
|||||||
STICKER
|
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
|
* For gallery
|
||||||
*/
|
*/
|
||||||
@ -232,7 +245,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
|||||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||||
val resolvedUrl = when (mode) {
|
val resolvedUrl = when (mode) {
|
||||||
Mode.FULL_SIZE,
|
Mode.FULL_SIZE,
|
||||||
Mode.STICKER -> resolveUrl(data)
|
Mode.STICKER -> resolveUrl(data)
|
||||||
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||||
}
|
}
|
||||||
// Fallback to base url
|
// Fallback to base url
|
||||||
@ -300,7 +313,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
|||||||
finalHeight = min(maxImageWidth * height / width, maxImageHeight)
|
finalHeight = min(maxImageWidth * height / width, maxImageHeight)
|
||||||
finalWidth = finalHeight * width / height
|
finalWidth = finalHeight * width / height
|
||||||
}
|
}
|
||||||
Mode.STICKER -> {
|
Mode.STICKER -> {
|
||||||
// limit on width
|
// limit on width
|
||||||
val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2)
|
val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2)
|
||||||
finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp)
|
finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp)
|
||||||
|
@ -783,6 +783,15 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||||||
return defaultPrefs.getBoolean(SETTINGS_USE_ANALYTICS_KEY, false)
|
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.
|
* Enable or disable the analytics tracking.
|
||||||
*
|
*
|
||||||
|
@ -22,7 +22,6 @@ import android.widget.CheckedTextView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.SwitchPreference
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.restart
|
import im.vector.app.core.extensions.restart
|
||||||
import im.vector.app.core.preference.VectorListPreference
|
import im.vector.app.core.preference.VectorListPreference
|
||||||
@ -64,9 +63,9 @@ class VectorSettingsPreferencesFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Url preview
|
// Url preview
|
||||||
|
/*
|
||||||
|
TODO Note: we keep the setting client side for now
|
||||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SHOW_URL_PREVIEW_KEY)!!.let {
|
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SHOW_URL_PREVIEW_KEY)!!.let {
|
||||||
/*
|
|
||||||
TODO
|
|
||||||
it.isChecked = session.isURLPreviewEnabled
|
it.isChecked = session.isURLPreviewEnabled
|
||||||
|
|
||||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||||
@ -100,8 +99,8 @@ class VectorSettingsPreferencesFragment @Inject constructor(
|
|||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// update keep medias period
|
// update keep medias period
|
||||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_MEDIA_SAVING_PERIOD_KEY)!!.let {
|
findPreference<VectorPreference>(VectorPreferences.SETTINGS_MEDIA_SAVING_PERIOD_KEY)!!.let {
|
||||||
|
10
vector/src/main/res/drawable/ic_close_24dp.xml
Normal file
10
vector/src/main/res/drawable/ic_close_24dp.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="25dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="25"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M17.7929,5.2929C18.1834,4.9024 18.8166,4.9024 19.2071,5.2929C19.5976,5.6834 19.5976,6.3166 19.2071,6.7071L13.9142,12L19.2071,17.2929C19.5976,17.6834 19.5976,18.3166 19.2071,18.7071C18.8166,19.0976 18.1834,19.0976 17.7929,18.7071L12.5,13.4142L7.2071,18.7071C6.8166,19.0976 6.1834,19.0976 5.7929,18.7071C5.4024,18.3166 5.4024,17.6834 5.7929,17.2929L11.0858,12L5.7929,6.7071C5.4024,6.3166 5.4024,5.6834 5.7929,5.2929C6.1834,4.9024 6.8166,4.9024 7.2071,5.2929L12.5,10.5858L17.7929,5.2929Z" />
|
||||||
|
</vector>
|
@ -87,7 +87,6 @@
|
|||||||
android:id="@+id/messageContentTextStub"
|
android:id="@+id/messageContentTextStub"
|
||||||
style="@style/TimelineContentStubBaseParams"
|
style="@style/TimelineContentStubBaseParams"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inflatedId="@id/messageTextView"
|
|
||||||
android:layout="@layout/item_timeline_event_text_message_stub"
|
android:layout="@layout/item_timeline_event_text_message_stub"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
@ -1,9 +1,26 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/messageTextView"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="?riotx_text_primary"
|
android:orientation="vertical">
|
||||||
android:textSize="14sp"
|
|
||||||
tools:text="@sample/matrix.json/data/message" />
|
<TextView
|
||||||
|
android:id="@+id/messageTextView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="@sample/matrix.json/data/message" />
|
||||||
|
|
||||||
|
<im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
|
||||||
|
android:id="@+id/messageUrlPreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
88
vector/src/main/res/layout/url_preview.xml
Normal file
88
vector/src/main/res/layout/url_preview.xml
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/informationUrlPreviewContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/url_preview_left_border"
|
||||||
|
android:layout_width="2dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?riotx_text_tertiary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/url_preview_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="7dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/url_preview_close"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/url_preview_left_border"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Jo Malone denounces her former brand's John Boyega decision" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/url_preview_image"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="157dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:scaleType="fitStart"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/url_preview_title"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/url_preview_title"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/url_preview_description"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="7dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="4"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/url_preview_left_border"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/url_preview_image"
|
||||||
|
tools:text="The British perfumer says removing actor John Boyega from his own advert was “utterly despicable”." />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/url_preview_site"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="7dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textColor="?riotx_text_tertiary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/url_preview_left_border"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/url_preview_description"
|
||||||
|
tools:text="BBC News" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/url_preview_close"
|
||||||
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_close_24dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="?riotx_text_secondary"
|
||||||
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
</merge>
|
@ -107,6 +107,13 @@
|
|||||||
<color name="riotx_text_secondary_dark">#FFA1B2D1</color>
|
<color name="riotx_text_secondary_dark">#FFA1B2D1</color>
|
||||||
<color name="riotx_text_secondary_black">#FFA1B2D1</color>
|
<color name="riotx_text_secondary_black">#FFA1B2D1</color>
|
||||||
|
|
||||||
|
<attr name="riotx_text_tertiary" format="color" />
|
||||||
|
<color name="riotx_text_tertiary_light">#FF8D99A5</color>
|
||||||
|
<!-- TODO Pick color from Figma, I do not know where to find it -->
|
||||||
|
<color name="riotx_text_tertiary_dark">#FF8D99A5</color>
|
||||||
|
<!-- TODO Pick color from Figma, I do not know where to find it -->
|
||||||
|
<color name="riotx_text_tertiary_black">#FF8D99A5</color>
|
||||||
|
|
||||||
<attr name="riotx_text_primary_body_contrast" format="color" />
|
<attr name="riotx_text_primary_body_contrast" format="color" />
|
||||||
<color name="riotx_text_primary_body_contrast_light">#FF61708B</color>
|
<color name="riotx_text_primary_body_contrast_light">#FF61708B</color>
|
||||||
<color name="riotx_text_primary_body_contrast_dark">#FFA1B2D1</color>
|
<color name="riotx_text_primary_body_contrast_dark">#FFA1B2D1</color>
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
<item name="riotx_header_panel_text_secondary">@color/riotx_header_panel_text_secondary_black</item>
|
<item name="riotx_header_panel_text_secondary">@color/riotx_header_panel_text_secondary_black</item>
|
||||||
<item name="riotx_text_primary">@color/riotx_text_primary_black</item>
|
<item name="riotx_text_primary">@color/riotx_text_primary_black</item>
|
||||||
<item name="riotx_text_secondary">@color/riotx_text_secondary_black</item>
|
<item name="riotx_text_secondary">@color/riotx_text_secondary_black</item>
|
||||||
|
<item name="riotx_text_tertiary">@color/riotx_text_tertiary_black</item>
|
||||||
<item name="riotx_text_primary_body_contrast">@color/riotx_text_primary_body_contrast_black</item>
|
<item name="riotx_text_primary_body_contrast">@color/riotx_text_primary_body_contrast_black</item>
|
||||||
<item name="riotx_android_secondary">@color/riotx_android_secondary_black</item>
|
<item name="riotx_android_secondary">@color/riotx_android_secondary_black</item>
|
||||||
<item name="riotx_search_placeholder">@color/riotx_search_placeholder_black</item>
|
<item name="riotx_search_placeholder">@color/riotx_search_placeholder_black</item>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
<item name="riotx_header_panel_text_secondary">@color/riotx_header_panel_text_secondary_dark</item>
|
<item name="riotx_header_panel_text_secondary">@color/riotx_header_panel_text_secondary_dark</item>
|
||||||
<item name="riotx_text_primary">@color/riotx_text_primary_dark</item>
|
<item name="riotx_text_primary">@color/riotx_text_primary_dark</item>
|
||||||
<item name="riotx_text_secondary">@color/riotx_text_secondary_dark</item>
|
<item name="riotx_text_secondary">@color/riotx_text_secondary_dark</item>
|
||||||
|
<item name="riotx_text_tertiary">@color/riotx_text_tertiary_dark</item>
|
||||||
<item name="riotx_text_primary_body_contrast">@color/riotx_text_primary_body_contrast_dark</item>
|
<item name="riotx_text_primary_body_contrast">@color/riotx_text_primary_body_contrast_dark</item>
|
||||||
<item name="riotx_android_secondary">@color/riotx_android_secondary_dark</item>
|
<item name="riotx_android_secondary">@color/riotx_android_secondary_dark</item>
|
||||||
<item name="riotx_search_placeholder">@color/riotx_search_placeholder_dark</item>
|
<item name="riotx_search_placeholder">@color/riotx_search_placeholder_dark</item>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
<item name="riotx_header_panel_text_secondary">@color/riotx_header_panel_text_secondary_light</item>
|
<item name="riotx_header_panel_text_secondary">@color/riotx_header_panel_text_secondary_light</item>
|
||||||
<item name="riotx_text_primary">@color/riotx_text_primary_light</item>
|
<item name="riotx_text_primary">@color/riotx_text_primary_light</item>
|
||||||
<item name="riotx_text_secondary">@color/riotx_text_secondary_light</item>
|
<item name="riotx_text_secondary">@color/riotx_text_secondary_light</item>
|
||||||
|
<item name="riotx_text_tertiary">@color/riotx_text_tertiary_light</item>
|
||||||
<item name="riotx_text_primary_body_contrast">@color/riotx_text_primary_body_contrast_light</item>
|
<item name="riotx_text_primary_body_contrast">@color/riotx_text_primary_body_contrast_light</item>
|
||||||
<item name="riotx_android_secondary">@color/riotx_android_secondary_light</item>
|
<item name="riotx_android_secondary">@color/riotx_android_secondary_light</item>
|
||||||
<item name="riotx_search_placeholder">@color/riotx_search_placeholder_light</item>
|
<item name="riotx_search_placeholder">@color/riotx_search_placeholder_light</item>
|
||||||
|
@ -57,8 +57,7 @@
|
|||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"
|
android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"
|
||||||
android:summary="@string/settings_inline_url_preview_summary"
|
android:summary="@string/settings_inline_url_preview_summary"
|
||||||
android:title="@string/settings_inline_url_preview"
|
android:title="@string/settings_inline_url_preview" />
|
||||||
app:isPreferenceVisible="@bool/false_not_implemented" />
|
|
||||||
|
|
||||||
<im.vector.app.core.preference.VectorSwitchPreference
|
<im.vector.app.core.preference.VectorSwitchPreference
|
||||||
android:key="SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
|
android:key="SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
|
||||||
|
Loading…
Reference in New Issue
Block a user