Merge tag 'v1.5.32' into sc
Note: mainly conflicts in differing implementations to fix duplicate read receipts. After a quick glance at the upstream implementation, my first impression is that we likely want to keep our downstream implementation: - Better performance by checking for the more recent receipt in the SDK upon receipt receival, instead on rendering the timeline - null seems to always map to main, which caused us jumping receipts in the past, which we have addressed by now Conflicts: matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt Change-Id: I47ae2afe5fcbe77a4fec69374d83821b94a431de
This commit is contained in:
commit
acdf419768
|
@ -11,7 +11,7 @@ jobs:
|
||||||
- run: |
|
- run: |
|
||||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||||
- name: Danger
|
- name: Danger
|
||||||
uses: danger/danger-js@11.2.4
|
uses: danger/danger-js@11.2.6
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -66,7 +66,7 @@ jobs:
|
||||||
yarn add danger-plugin-lint-report --dev
|
yarn add danger-plugin-lint-report --dev
|
||||||
- name: Danger lint
|
- name: Danger lint
|
||||||
if: always()
|
if: always()
|
||||||
uses: danger/danger-js@11.2.4
|
uses: danger/danger-js@11.2.6
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -23,7 +23,7 @@ jobs:
|
||||||
- name: Run Emoji script
|
- name: Run Emoji script
|
||||||
run: ./tools/import_emojis.py
|
run: ./tools/import_emojis.py
|
||||||
- name: Create Pull Request for Emojis
|
- name: Create Pull Request for Emojis
|
||||||
uses: peter-evans/create-pull-request@v4
|
uses: peter-evans/create-pull-request@v5
|
||||||
with:
|
with:
|
||||||
commit-message: Sync Emojis
|
commit-message: Sync Emojis
|
||||||
title: Sync Emojis
|
title: Sync Emojis
|
||||||
|
@ -49,7 +49,7 @@ jobs:
|
||||||
- name: Run SAS String script
|
- name: Run SAS String script
|
||||||
run: ./tools/import_sas_strings.py
|
run: ./tools/import_sas_strings.py
|
||||||
- name: Create Pull Request for SAS Strings
|
- name: Create Pull Request for SAS Strings
|
||||||
uses: peter-evans/create-pull-request@v4
|
uses: peter-evans/create-pull-request@v5
|
||||||
with:
|
with:
|
||||||
commit-message: Sync SAS Strings
|
commit-message: Sync SAS Strings
|
||||||
title: Sync SAS Strings
|
title: Sync SAS Strings
|
||||||
|
@ -68,7 +68,7 @@ jobs:
|
||||||
- name: Run analytics import script
|
- name: Run analytics import script
|
||||||
run: ./tools/import_analytic_plan.sh
|
run: ./tools/import_analytic_plan.sh
|
||||||
- name: Create Pull Request for analytics plan
|
- name: Create Pull Request for analytics plan
|
||||||
uses: peter-evans/create-pull-request@v4
|
uses: peter-evans/create-pull-request@v5
|
||||||
with:
|
with:
|
||||||
commit-message: Sync analytics plan
|
commit-message: Sync analytics plan
|
||||||
title: Sync analytics plan
|
title: Sync analytics plan
|
||||||
|
|
10
CHANGES.md
10
CHANGES.md
|
@ -1,3 +1,13 @@
|
||||||
|
Changes in Element v1.5.32 (2023-04-19)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Fix multiple read receipts for the same user in timeline. ([#7882](https://github.com/vector-im/element-android/issues/7882))
|
||||||
|
- The new permalink rendering is not applied on permalink created with the potential clientPermalinkBaseUrl ([#8307](https://github.com/vector-im/element-android/issues/8307))
|
||||||
|
- Keep screen on while recording voicebroadcast ([#8313](https://github.com/vector-im/element-android/issues/8313))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.5.30 (2023-04-05)
|
Changes in Element v1.5.30 (2023-04-05)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ def markwon = "4.6.2"
|
||||||
def moshi = "1.14.0"
|
def moshi = "1.14.0"
|
||||||
def lifecycle = "2.5.1"
|
def lifecycle = "2.5.1"
|
||||||
def flowBinding = "1.2.0"
|
def flowBinding = "1.2.0"
|
||||||
def flipper = "0.188.0"
|
def flipper = "0.189.0"
|
||||||
def epoxy = "5.0.0"
|
def epoxy = "5.0.0"
|
||||||
def mavericks = "3.0.2"
|
def mavericks = "3.0.2"
|
||||||
def glide = "4.14.2"
|
def glide = "4.14.2"
|
||||||
|
@ -27,7 +27,7 @@ def jjwt = "0.11.5"
|
||||||
def vanniktechEmoji = "0.16.0"
|
def vanniktechEmoji = "0.16.0"
|
||||||
def sentry = "6.17.0"
|
def sentry = "6.17.0"
|
||||||
// Use 1.6.0 alpha to fix issue with test
|
// Use 1.6.0 alpha to fix issue with test
|
||||||
def fragment = "1.6.0-alpha08"
|
def fragment = "1.6.0-alpha09"
|
||||||
// Testing
|
// Testing
|
||||||
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
|
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
|
||||||
def espresso = "3.5.1"
|
def espresso = "3.5.1"
|
||||||
|
@ -51,14 +51,14 @@ ext.libs = [
|
||||||
'activity' : "androidx.activity:activity-ktx:1.7.0",
|
'activity' : "androidx.activity:activity-ktx:1.7.0",
|
||||||
'appCompat' : "androidx.appcompat:appcompat:1.6.1",
|
'appCompat' : "androidx.appcompat:appcompat:1.6.1",
|
||||||
'biometric' : "androidx.biometric:biometric:1.1.0",
|
'biometric' : "androidx.biometric:biometric:1.1.0",
|
||||||
'core' : "androidx.core:core-ktx:1.9.0",
|
'core' : "androidx.core:core-ktx:1.10.0",
|
||||||
'recyclerview' : "androidx.recyclerview:recyclerview:1.3.0",
|
'recyclerview' : "androidx.recyclerview:recyclerview:1.3.0",
|
||||||
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.6",
|
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.6",
|
||||||
'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment",
|
'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment",
|
||||||
'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment",
|
'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment",
|
||||||
'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment",
|
'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment",
|
||||||
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
|
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4",
|
||||||
'work' : "androidx.work:work-runtime-ktx:2.8.0",
|
'work' : "androidx.work:work-runtime-ktx:2.8.1",
|
||||||
'autoFill' : "androidx.autofill:autofill:1.1.0",
|
'autoFill' : "androidx.autofill:autofill:1.1.0",
|
||||||
'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0",
|
'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0",
|
||||||
'junit' : "androidx.test.ext:junit:1.1.5",
|
'junit' : "androidx.test.ext:junit:1.1.5",
|
||||||
|
@ -87,7 +87,7 @@ ext.libs = [
|
||||||
//'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
|
//'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
|
||||||
//'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
|
//'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.8"
|
'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.10"
|
||||||
],
|
],
|
||||||
dagger : [
|
dagger : [
|
||||||
'dagger' : "com.google.dagger:dagger:$dagger",
|
'dagger' : "com.google.dagger:dagger:$dagger",
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Mainly bugfixing.
|
||||||
|
Full changelog: https://github.com/vector-im/element-android/releases
|
|
@ -62,7 +62,7 @@ android {
|
||||||
// that the app's state is completely cleared between tests.
|
// that the app's state is completely cleared between tests.
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
|
||||||
buildConfigField "String", "SDK_VERSION", "\"1.5.30\""
|
buildConfigField "String", "SDK_VERSION", "\"1.5.32\""
|
||||||
|
|
||||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||||
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
|
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
|
||||||
|
|
|
@ -48,4 +48,8 @@ class TestPermalinkService : PermalinkService {
|
||||||
MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
|
MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isPermalinkSupported(supportedHosts: Array<String>, url: String): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ object MatrixPatterns {
|
||||||
// regex pattern to find permalink with message id.
|
// regex pattern to find permalink with message id.
|
||||||
// Android does not support in URL so extract it.
|
// Android does not support in URL so extract it.
|
||||||
private const val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/"
|
private const val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/"
|
||||||
private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/"
|
private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/#/(room|user)/"
|
||||||
const val SEP_REGEX = "/"
|
const val SEP_REGEX = "/"
|
||||||
|
|
||||||
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK = PERMALINK_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK = PERMALINK_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||||
|
|
|
@ -97,4 +97,15 @@ interface PermalinkService {
|
||||||
* @return the created template
|
* @return the created template
|
||||||
*/
|
*/
|
||||||
fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String
|
fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the url is a permalink. It must be a matrix.to link
|
||||||
|
* or a link with host provided by the string-array `permalink_supported_hosts` in the config file
|
||||||
|
*
|
||||||
|
* @param supportedHosts the list of hosts supported for permalinks
|
||||||
|
* @param url the link to check, Ex: "https://matrix.to/#/@benoit:matrix.org"
|
||||||
|
*
|
||||||
|
* @return true when url is a permalink
|
||||||
|
*/
|
||||||
|
fun isPermalinkSupported(supportedHosts: Array<String>, url: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import io.realm.kotlin.createObject
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.read.ReadService
|
||||||
import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN
|
import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN
|
||||||
import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN_OR_NULL
|
import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN_OR_NULL
|
||||||
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
|
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
|
||||||
|
@ -80,7 +81,7 @@ internal fun ChunkEntity.addTimelineEvent(
|
||||||
val senderId = eventEntity.sender ?: ""
|
val senderId = eventEntity.sender ?: ""
|
||||||
|
|
||||||
// Update RR for the sender of a new message with a dummy one
|
// Update RR for the sender of a new message with a dummy one
|
||||||
val readReceiptsSummaryEntity = if (!ownedByThreadChunk) handleReadReceipts(realm, roomId, eventEntity, senderId) else null
|
val readReceiptsSummaryEntity = handleReadReceiptsOfSender(realm, roomId, eventEntity, senderId)
|
||||||
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
|
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
|
||||||
this.localId = localId
|
this.localId = localId
|
||||||
this.root = eventEntity
|
this.root = eventEntity
|
||||||
|
@ -130,7 +131,7 @@ internal fun computeIsUnique(
|
||||||
|
|
||||||
private val rrDimber = Dimber("ReadReceipts", DbgUtil.DBG_READ_RECEIPTS)
|
private val rrDimber = Dimber("ReadReceipts", DbgUtil.DBG_READ_RECEIPTS)
|
||||||
|
|
||||||
private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
|
private fun handleReadReceiptsOfSender(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
|
||||||
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
|
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
|
||||||
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {
|
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {
|
||||||
this.roomId = roomId
|
this.roomId = roomId
|
||||||
|
@ -140,12 +141,20 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE
|
||||||
val timestampOfEvent = originServerTs.toDouble()
|
val timestampOfEvent = originServerTs.toDouble()
|
||||||
// SC: fight duplicate read receipts in main timeline
|
// SC: fight duplicate read receipts in main timeline
|
||||||
val receiptDestinations = if (eventEntity.rootThreadEventId in listOf(null, THREAD_ID_MAIN)) {
|
val receiptDestinations = if (eventEntity.rootThreadEventId in listOf(null, THREAD_ID_MAIN)) {
|
||||||
|
// Upstream v1.3.32 now does ?: THREAD_ID_MAIN for this... but that might cause some issues with jumping receipts
|
||||||
|
// that we had before, so stick to our approach for now...?
|
||||||
|
// setOf(eventEntity.rootThreadEventId ?: THREAD_ID_MAIN, THREAD_ID_MAIN_OR_NULL)
|
||||||
setOf(eventEntity.rootThreadEventId, THREAD_ID_MAIN_OR_NULL)
|
setOf(eventEntity.rootThreadEventId, THREAD_ID_MAIN_OR_NULL)
|
||||||
} else {
|
} else {
|
||||||
setOf(eventEntity.rootThreadEventId)
|
setOf(eventEntity.rootThreadEventId)
|
||||||
}
|
}
|
||||||
receiptDestinations.forEach { rootThreadEventId ->
|
receiptDestinations.forEach { rootThreadEventId ->
|
||||||
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = rootThreadEventId)
|
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(
|
||||||
|
realm = realm,
|
||||||
|
roomId = roomId,
|
||||||
|
userId = senderId,
|
||||||
|
threadId = rootThreadEventId
|
||||||
|
)
|
||||||
val shouldForceMon: Boolean
|
val shouldForceMon: Boolean
|
||||||
val shouldSkipMon: Boolean
|
val shouldSkipMon: Boolean
|
||||||
if (rootThreadEventId == THREAD_ID_MAIN_OR_NULL) {
|
if (rootThreadEventId == THREAD_ID_MAIN_OR_NULL) {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.permalinks
|
package org.matrix.android.sdk.internal.session.permalinks
|
||||||
|
|
||||||
|
import androidx.core.net.toUri
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -47,4 +48,9 @@ internal class DefaultPermalinkService @Inject constructor(
|
||||||
override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
|
override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
|
||||||
return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo)
|
return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isPermalinkSupported(supportedHosts: Array<String>, url: String): Boolean {
|
||||||
|
return url.startsWith(PermalinkService.MATRIX_TO_URL_BASE) ||
|
||||||
|
supportedHosts.any { url.toUri().host == it }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2599,7 +2599,9 @@
|
||||||
"face",
|
"face",
|
||||||
"shaking",
|
"shaking",
|
||||||
"shock",
|
"shock",
|
||||||
"vibrate"
|
"vibrate",
|
||||||
|
"dizzy",
|
||||||
|
"blurry"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"relieved-face": {
|
"relieved-face": {
|
||||||
|
@ -3800,7 +3802,8 @@
|
||||||
"heart",
|
"heart",
|
||||||
"like",
|
"like",
|
||||||
"love",
|
"love",
|
||||||
"pink"
|
"pink",
|
||||||
|
"valentines"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"orange-heart": {
|
"orange-heart": {
|
||||||
|
@ -3854,8 +3857,9 @@
|
||||||
"cyan",
|
"cyan",
|
||||||
"heart",
|
"heart",
|
||||||
"light blue",
|
"light blue",
|
||||||
"light blue heart",
|
"teal",
|
||||||
"teal"
|
"ice",
|
||||||
|
"baby blue"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"purple-heart": {
|
"purple-heart": {
|
||||||
|
@ -3892,10 +3896,10 @@
|
||||||
"b": "1FA76",
|
"b": "1FA76",
|
||||||
"j": [
|
"j": [
|
||||||
"gray",
|
"gray",
|
||||||
"grey heart",
|
|
||||||
"heart",
|
"heart",
|
||||||
"silver",
|
"silver",
|
||||||
"slate"
|
"slate",
|
||||||
|
"monochrome"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"white-heart": {
|
"white-heart": {
|
||||||
|
@ -4198,11 +4202,12 @@
|
||||||
"j": [
|
"j": [
|
||||||
"high five",
|
"high five",
|
||||||
"leftward",
|
"leftward",
|
||||||
"leftwards pushing hand",
|
|
||||||
"push",
|
"push",
|
||||||
"refuse",
|
"refuse",
|
||||||
"stop",
|
"stop",
|
||||||
"wait"
|
"wait",
|
||||||
|
"highfive",
|
||||||
|
"pressing"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"rightwards-pushing-hand": {
|
"rightwards-pushing-hand": {
|
||||||
|
@ -4213,9 +4218,10 @@
|
||||||
"push",
|
"push",
|
||||||
"refuse",
|
"refuse",
|
||||||
"rightward",
|
"rightward",
|
||||||
"rightwards pushing hand",
|
|
||||||
"stop",
|
"stop",
|
||||||
"wait"
|
"wait",
|
||||||
|
"highfive",
|
||||||
|
"pressing"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ok-hand": {
|
"ok-hand": {
|
||||||
|
@ -8620,7 +8626,11 @@
|
||||||
"antlers",
|
"antlers",
|
||||||
"elk",
|
"elk",
|
||||||
"mammal",
|
"mammal",
|
||||||
"moose"
|
"shrek",
|
||||||
|
"canada",
|
||||||
|
"sweden",
|
||||||
|
"sven",
|
||||||
|
"cool"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"donkey": {
|
"donkey": {
|
||||||
|
@ -8630,10 +8640,10 @@
|
||||||
"animal",
|
"animal",
|
||||||
"ass",
|
"ass",
|
||||||
"burro",
|
"burro",
|
||||||
"donkey",
|
|
||||||
"mammal",
|
"mammal",
|
||||||
"mule",
|
"mule",
|
||||||
"stubborn"
|
"stubborn",
|
||||||
|
"eeyore"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"horse": {
|
"horse": {
|
||||||
|
@ -9311,7 +9321,8 @@
|
||||||
"bird",
|
"bird",
|
||||||
"flying",
|
"flying",
|
||||||
"mythology",
|
"mythology",
|
||||||
"wing"
|
"angel",
|
||||||
|
"birds"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"black-bird": {
|
"black-bird": {
|
||||||
|
@ -9331,9 +9342,10 @@
|
||||||
"j": [
|
"j": [
|
||||||
"bird",
|
"bird",
|
||||||
"fowl",
|
"fowl",
|
||||||
"goose",
|
|
||||||
"honk",
|
"honk",
|
||||||
"silly"
|
"silly",
|
||||||
|
"jemima",
|
||||||
|
"goosebumps"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"frog": {
|
"frog": {
|
||||||
|
@ -9581,10 +9593,11 @@
|
||||||
"burn",
|
"burn",
|
||||||
"invertebrate",
|
"invertebrate",
|
||||||
"jelly",
|
"jelly",
|
||||||
"jellyfish",
|
|
||||||
"marine",
|
"marine",
|
||||||
"ouch",
|
"ouch",
|
||||||
"stinger"
|
"stinger",
|
||||||
|
"sting",
|
||||||
|
"tentacles"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"snail": {
|
"snail": {
|
||||||
|
@ -9885,7 +9898,6 @@
|
||||||
"j": [
|
"j": [
|
||||||
"bluebonnet",
|
"bluebonnet",
|
||||||
"flower",
|
"flower",
|
||||||
"hyacinth",
|
|
||||||
"lavender",
|
"lavender",
|
||||||
"lupine",
|
"lupine",
|
||||||
"snapdragon"
|
"snapdragon"
|
||||||
|
@ -10423,9 +10435,11 @@
|
||||||
"b": "1FADA",
|
"b": "1FADA",
|
||||||
"j": [
|
"j": [
|
||||||
"beer",
|
"beer",
|
||||||
"ginger root",
|
|
||||||
"root",
|
"root",
|
||||||
"spice"
|
"spice",
|
||||||
|
"yellow",
|
||||||
|
"cooking",
|
||||||
|
"gingerbread"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"pea-pod": {
|
"pea-pod": {
|
||||||
|
@ -10437,7 +10451,9 @@
|
||||||
"legume",
|
"legume",
|
||||||
"pea",
|
"pea",
|
||||||
"pod",
|
"pod",
|
||||||
"vegetable"
|
"vegetable",
|
||||||
|
"cozy",
|
||||||
|
"green"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"bread": {
|
"bread": {
|
||||||
|
@ -15533,9 +15549,9 @@
|
||||||
"dance",
|
"dance",
|
||||||
"fan",
|
"fan",
|
||||||
"flutter",
|
"flutter",
|
||||||
"folding hand fan",
|
|
||||||
"hot",
|
"hot",
|
||||||
"shy"
|
"shy",
|
||||||
|
"flamenco"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"purse": {
|
"purse": {
|
||||||
|
@ -15719,7 +15735,8 @@
|
||||||
"Afro",
|
"Afro",
|
||||||
"comb",
|
"comb",
|
||||||
"hair",
|
"hair",
|
||||||
"pick"
|
"pick",
|
||||||
|
"afro"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"crown": {
|
"crown": {
|
||||||
|
@ -16165,7 +16182,6 @@
|
||||||
"b": "1FA87",
|
"b": "1FA87",
|
||||||
"j": [
|
"j": [
|
||||||
"instrument",
|
"instrument",
|
||||||
"maracas",
|
|
||||||
"music",
|
"music",
|
||||||
"percussion",
|
"percussion",
|
||||||
"rattle",
|
"rattle",
|
||||||
|
@ -16177,11 +16193,13 @@
|
||||||
"b": "1FA88",
|
"b": "1FA88",
|
||||||
"j": [
|
"j": [
|
||||||
"fife",
|
"fife",
|
||||||
"flute",
|
|
||||||
"music",
|
"music",
|
||||||
"pipe",
|
"pipe",
|
||||||
"recorder",
|
"recorder",
|
||||||
"woodwind"
|
"woodwind",
|
||||||
|
"bamboo",
|
||||||
|
"instrument",
|
||||||
|
"pied piper"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"mobile-phone": {
|
"mobile-phone": {
|
||||||
|
@ -19036,9 +19054,9 @@
|
||||||
"a": "⊛ Khanda",
|
"a": "⊛ Khanda",
|
||||||
"b": "1FAAF",
|
"b": "1FAAF",
|
||||||
"j": [
|
"j": [
|
||||||
"khanda",
|
|
||||||
"religion",
|
"religion",
|
||||||
"Sikh"
|
"Sikh",
|
||||||
|
"Sikhism"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"aries": {
|
"aries": {
|
||||||
|
@ -19460,7 +19478,9 @@
|
||||||
"computer",
|
"computer",
|
||||||
"internet",
|
"internet",
|
||||||
"network",
|
"network",
|
||||||
"wireless"
|
"wifi",
|
||||||
|
"contactless",
|
||||||
|
"signal"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"vibration-mode": {
|
"vibration-mode": {
|
||||||
|
|
|
@ -37,7 +37,7 @@ ext.versionMinor = 5
|
||||||
// Note: even values are reserved for regular release, odd values for hotfix release.
|
// Note: even values are reserved for regular release, odd values for hotfix release.
|
||||||
// When creating a hotfix, you should decrease the value, since the current value
|
// When creating a hotfix, you should decrease the value, since the current value
|
||||||
// is the value for the next regular release.
|
// is the value for the next regular release.
|
||||||
ext.versionPatch = 30
|
ext.versionPatch = 32
|
||||||
|
|
||||||
ext.scVersion = 66
|
ext.scVersion = 66
|
||||||
|
|
||||||
|
|
|
@ -133,7 +133,7 @@ dependencies {
|
||||||
implementation libs.androidx.biometric
|
implementation libs.androidx.biometric
|
||||||
|
|
||||||
api "org.threeten:threetenbp:1.4.0:no-tzdb"
|
api "org.threeten:threetenbp:1.4.0:no-tzdb"
|
||||||
api "com.gabrielittner.threetenbp:lazythreetenbp:0.14.0"
|
api "com.gabrielittner.threetenbp:lazythreetenbp:0.16.0"
|
||||||
|
|
||||||
implementation libs.squareup.moshi
|
implementation libs.squareup.moshi
|
||||||
implementation libs.squareup.moshiKt
|
implementation libs.squareup.moshiKt
|
||||||
|
@ -311,7 +311,7 @@ dependencies {
|
||||||
// Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868
|
// Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868
|
||||||
// Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0)
|
// Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0)
|
||||||
//noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26.
|
//noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26.
|
||||||
implementation "org.checkerframework:checker:3.32.0"
|
implementation "org.checkerframework:checker:3.33.0"
|
||||||
|
|
||||||
androidTestImplementation libs.androidx.testCore
|
androidTestImplementation libs.androidx.testCore
|
||||||
androidTestImplementation libs.androidx.testRunner
|
androidTestImplementation libs.androidx.testRunner
|
||||||
|
|
|
@ -184,6 +184,7 @@ class HomeActivityViewModel @AssistedInject constructor(
|
||||||
if (analyticsConfig.isEnabled) {
|
if (analyticsConfig.isEnabled) {
|
||||||
analyticsStore.didAskUserConsentFlow
|
analyticsStore.didAskUserConsentFlow
|
||||||
.onEach { didAskUser ->
|
.onEach { didAskUser ->
|
||||||
|
Timber.v("DidAskUserConsent: $didAskUser")
|
||||||
if (!didAskUser) {
|
if (!didAskUser) {
|
||||||
_viewEvents.post(HomeActivityViewEvents.ShowAnalyticsOptIn)
|
_viewEvents.post(HomeActivityViewEvents.ShowAnalyticsOptIn)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -152,7 +152,6 @@ import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
|
||||||
import im.vector.app.features.home.room.detail.composer.boolean
|
import im.vector.app.features.home.room.detail.composer.boolean
|
||||||
import im.vector.app.features.home.room.detail.composer.voice.VoiceRecorderFragment
|
import im.vector.app.features.home.room.detail.composer.voice.VoiceRecorderFragment
|
||||||
import im.vector.app.features.home.room.detail.error.RoomNotFound
|
import im.vector.app.features.home.room.detail.error.RoomNotFound
|
||||||
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction
|
import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction
|
||||||
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
||||||
|
@ -172,6 +171,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
|
||||||
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.reactions.ViewReactionsBottomSheet
|
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.readreceipts.DisplayReadReceiptsBottomSheet
|
||||||
import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever
|
import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever
|
||||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||||
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
|
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
|
||||||
|
|
|
@ -1442,13 +1442,17 @@ class TimelineViewModel @AssistedInject constructor(
|
||||||
computeUnreadState(timelineEvents, roomSummary)
|
computeUnreadState(timelineEvents, roomSummary)
|
||||||
}
|
}
|
||||||
// We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread
|
// We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread
|
||||||
// However, we want to update an existing HasUnread, if the readMarkerId hasn't changed,
|
// However, we want to update an existing HasUnread, if the readMarkerId hasn't changed or when we go back in live,
|
||||||
// as we might be loading new events to fill gaps in the timeline.
|
// as we might be loading new events to fill gaps in the timeline.
|
||||||
.distinctUntilChanged { previous, current ->
|
.distinctUntilChanged { previous, current ->
|
||||||
when {
|
when {
|
||||||
previous is UnreadState.Unknown || previous is UnreadState.ReadMarkerNotLoaded -> false
|
previous is UnreadState.Unknown || previous is UnreadState.ReadMarkerNotLoaded -> false
|
||||||
previous is UnreadState.HasUnread && current is UnreadState.HasUnread &&
|
previous is UnreadState.HasUnread && current is UnreadState.HasUnread &&
|
||||||
previous.readMarkerId == current.readMarkerId -> false
|
previous.readMarkerId == current.readMarkerId -> false
|
||||||
|
previous is UnreadState.HasUnread && (
|
||||||
|
current is UnreadState.HasUnread && previous.firstUnreadEventId != current.firstUnreadEventId ||
|
||||||
|
current is UnreadState.HasNoUnread
|
||||||
|
) && timeline?.isLive.orFalse() -> false
|
||||||
current is UnreadState.HasUnread || current is UnreadState.HasNoUnread -> true
|
current is UnreadState.HasUnread || current is UnreadState.HasNoUnread -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
|
@ -239,7 +239,7 @@ class AudioMessageHelper @Inject constructor(
|
||||||
val percentage = currentPosition.toFloat() / totalDuration
|
val percentage = currentPosition.toFloat() / totalDuration
|
||||||
playbackTracker.updatePlayingAtPlaybackTime(id, currentPosition, percentage)
|
playbackTracker.updatePlayingAtPlaybackTime(id, currentPosition, percentage)
|
||||||
} else {
|
} else {
|
||||||
playbackTracker.stopPlayback(id)
|
playbackTracker.stopPlaybackOrRecorder(id)
|
||||||
stopPlaybackTicker()
|
stopPlaybackTicker()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
|
import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
|
||||||
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.ReadReceiptsItem
|
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.TypingItem_
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.readreceipts.ReadReceiptsCache
|
||||||
import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever
|
import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever
|
||||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||||
import im.vector.app.features.media.AttachmentData
|
import im.vector.app.features.media.AttachmentData
|
||||||
|
@ -213,7 +215,7 @@ class TimelineEventController @Inject constructor(
|
||||||
// Map eventId to adapter position
|
// Map eventId to adapter position
|
||||||
private val adapterPositionMapping = HashMap<String, Int>()
|
private val adapterPositionMapping = HashMap<String, Int>()
|
||||||
private val timelineEventsGroups = TimelineEventsGroups()
|
private val timelineEventsGroups = TimelineEventsGroups()
|
||||||
private val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
|
private val readReceiptsCache = ReadReceiptsCache()
|
||||||
private val modelCache = arrayListOf<CacheItemData?>()
|
private val modelCache = arrayListOf<CacheItemData?>()
|
||||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||||
private var inSubmitList: Boolean = false
|
private var inSubmitList: Boolean = false
|
||||||
|
@ -463,7 +465,7 @@ class TimelineEventController @Inject constructor(
|
||||||
}
|
}
|
||||||
Timber.v("Preprocess events took $preprocessEventsTiming ms")
|
Timber.v("Preprocess events took $preprocessEventsTiming ms")
|
||||||
var numberOfEventsToBuild = 0
|
var numberOfEventsToBuild = 0
|
||||||
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvent)
|
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(readReceiptsCache.receiptsByEvent())
|
||||||
(0 until modelCache.size).forEach { position ->
|
(0 until modelCache.size).forEach { position ->
|
||||||
val event = currentSnapshot[position]
|
val event = currentSnapshot[position]
|
||||||
val nextEvent = currentSnapshot.nextOrNull(position)
|
val nextEvent = currentSnapshot.nextOrNull(position)
|
||||||
|
@ -509,7 +511,7 @@ class TimelineEventController @Inject constructor(
|
||||||
}
|
}
|
||||||
val itemCachedData = modelCache[position] ?: return@forEach
|
val itemCachedData = modelCache[position] ?: return@forEach
|
||||||
// Then update with additional models if needed
|
// Then update with additional models if needed
|
||||||
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvent)
|
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, readReceiptsCache.receiptsByEvent())
|
||||||
}
|
}
|
||||||
Timber.v("Number of events to rebuild: $numberOfEventsToBuild on ${modelCache.size} total events")
|
Timber.v("Number of events to rebuild: $numberOfEventsToBuild on ${modelCache.size} total events")
|
||||||
}
|
}
|
||||||
|
@ -598,15 +600,15 @@ class TimelineEventController @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preprocessReverseEvents() {
|
private fun preprocessReverseEvents() {
|
||||||
receiptsByEvent.clear()
|
readReceiptsCache.clear()
|
||||||
timelineEventsGroups.clear()
|
timelineEventsGroups.clear()
|
||||||
val itr = currentSnapshot.listIterator(currentSnapshot.size)
|
val itr = currentSnapshot.listIterator(currentSnapshot.size)
|
||||||
var lastShownEventId: String? = null
|
var lastShownEventId: String? = null
|
||||||
while (itr.hasPrevious()) {
|
while (itr.hasPrevious()) {
|
||||||
val event = itr.previous()
|
val event = itr.previous()
|
||||||
timelineEventsGroups.addOrIgnore(event)
|
timelineEventsGroups.addOrIgnore(event)
|
||||||
val currentReadReceipts = ArrayList(event.readReceipts).filter {
|
val currentReadReceipts = event.readReceipts.filter {
|
||||||
it.roomMember.userId != session.myUserId && it.isVisibleInThisThread()
|
it.roomMember.userId != session.myUserId
|
||||||
}
|
}
|
||||||
if (timelineEventVisibilityHelper.shouldShowEvent(
|
if (timelineEventVisibilityHelper.shouldShowEvent(
|
||||||
timelineEvent = event,
|
timelineEvent = event,
|
||||||
|
@ -619,18 +621,7 @@ class TimelineEventController @Inject constructor(
|
||||||
if (lastShownEventId == null) {
|
if (lastShownEventId == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() }
|
readReceiptsCache.addReceiptsOnEvent(currentReadReceipts, lastShownEventId)
|
||||||
existingReceipts.addAll(currentReadReceipts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ReadReceipt.isVisibleInThisThread(): Boolean {
|
|
||||||
return if (partialState.isFromThreadTimeline()) {
|
|
||||||
this.threadId == partialState.rootThreadEventId
|
|
||||||
} else if (DbgUtil.isDbgEnabled(DbgUtil.DBG_SHOW_DUPLICATE_READ_RECEIPTS)) {
|
|
||||||
this.threadId in listOf(null, ReadService.THREAD_ID_MAIN, ReadService.THREAD_ID_MAIN_OR_NULL)
|
|
||||||
} else {
|
|
||||||
this.threadId == ReadService.THREAD_ID_MAIN_OR_NULL
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,7 +100,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopPlayback(id: String) {
|
fun stopPlaybackOrRecorder(id: String) {
|
||||||
val state = getPlaybackState(id)
|
val state = getPlaybackState(id)
|
||||||
if (state !is Listener.State.Error) {
|
if (state !is Listener.State.Error) {
|
||||||
setState(id, Listener.State.Idle)
|
setState(id, Listener.State.Idle)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright (c) 2023 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.
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.readreceipts
|
package im.vector.app.features.home.room.detail.timeline.readreceipts
|
||||||
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright (c) 2023 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.
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.readreceipts
|
package im.vector.app.features.home.room.detail.timeline.readreceipts
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright (c) 2023 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.
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.readreceipts
|
package im.vector.app.features.home.room.detail.timeline.readreceipts
|
||||||
|
|
||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
import de.spiritcroc.matrixsdk.util.DbgUtil
|
import de.spiritcroc.matrixsdk.util.DbgUtil
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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.readreceipts
|
||||||
|
|
||||||
|
import im.vector.lib.core.utils.compat.removeIfCompat
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||||
|
|
||||||
|
class ReadReceiptsCache {
|
||||||
|
|
||||||
|
private val receiptsByEventId = HashMap<String, MutableList<ReadReceipt>>()
|
||||||
|
|
||||||
|
// Key is userId, Value is eventId
|
||||||
|
private val receiptEventIdByUserId = HashMap<String, String>()
|
||||||
|
|
||||||
|
fun receiptsByEvent(): Map<String, List<ReadReceipt>> {
|
||||||
|
return receiptsByEventId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addReceiptsOnEvent(receipts: List<ReadReceipt>, eventId: String) {
|
||||||
|
val existingReceipts = receiptsByEventId.getOrPut(eventId) { ArrayList() }
|
||||||
|
receipts.forEach { readReceipt ->
|
||||||
|
val receiptUserId = readReceipt.roomMember.userId
|
||||||
|
val receiptEventId = receiptEventIdByUserId[receiptUserId]
|
||||||
|
// If we already have a read receipt for this user, move it so we only
|
||||||
|
// use the most recent. It can happen because of threaded read receipts.
|
||||||
|
if (receiptEventId != null) {
|
||||||
|
receiptsByEventId[receiptEventId]?.removeIfCompat {
|
||||||
|
it.roomMember.userId == receiptUserId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receiptEventIdByUserId[receiptUserId] = eventId
|
||||||
|
existingReceipts.add(readReceipt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
receiptsByEventId.clear()
|
||||||
|
receiptEventIdByUserId.clear()
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,7 +29,7 @@ import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.glide.GlideApp
|
import im.vector.app.core.glide.GlideApp
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.html.PillImageSpan
|
import im.vector.app.features.html.PillImageSpan
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||||
import org.matrix.android.sdk.api.session.getUserOrDefault
|
import org.matrix.android.sdk.api.session.getUserOrDefault
|
||||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
||||||
|
@ -99,7 +99,9 @@ class EventTextRenderer @AssistedInject constructor(
|
||||||
private fun addPermalinksSpans(text: Spannable) {
|
private fun addPermalinksSpans(text: Spannable) {
|
||||||
for (match in Patterns.WEB_URL.toRegex().findAll(text)) {
|
for (match in Patterns.WEB_URL.toRegex().findAll(text)) {
|
||||||
val url = text.substring(match.range)
|
val url = text.substring(match.range)
|
||||||
val matrixItem = if (MatrixPatterns.isPermalink(url)) {
|
val supportedHosts = context.resources.getStringArray(R.array.permalink_supported_hosts)
|
||||||
|
val isPermalinkSupported = sessionHolder.getSafeActiveSession()?.permalinkService()?.isPermalinkSupported(supportedHosts, url).orFalse()
|
||||||
|
val matrixItem = if (isPermalinkSupported) {
|
||||||
when (val permalinkData = PermalinkParser.parse(url)) {
|
when (val permalinkData = PermalinkParser.parse(url)) {
|
||||||
is PermalinkData.UserLink -> permalinkData.toMatrixItem()
|
is PermalinkData.UserLink -> permalinkData.toMatrixItem()
|
||||||
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
|
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
|
||||||
|
|
|
@ -22,11 +22,13 @@ import android.text.Spanned
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.glide.GlideApp
|
import im.vector.app.core.glide.GlideApp
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import io.noties.markwon.core.spans.LinkSpan
|
import io.noties.markwon.core.spans.LinkSpan
|
||||||
import io.noties.markwon.image.AsyncDrawableSpan
|
import io.noties.markwon.image.AsyncDrawableSpan
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||||
import org.matrix.android.sdk.api.session.getUser
|
import org.matrix.android.sdk.api.session.getUser
|
||||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
||||||
|
@ -106,12 +108,18 @@ class PillsPostProcessor @AssistedInject constructor(
|
||||||
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
|
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
|
||||||
|
|
||||||
private fun LinkSpan.createPillSpan(): PillImageSpan? {
|
private fun LinkSpan.createPillSpan(): PillImageSpan? {
|
||||||
|
val supportedHosts = context.resources.getStringArray(R.array.permalink_supported_hosts)
|
||||||
|
val isPermalinkSupported = sessionHolder.getSafeActiveSession()?.permalinkService()?.isPermalinkSupported(supportedHosts, url).orFalse()
|
||||||
|
if (isPermalinkSupported) {
|
||||||
val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) {
|
val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) {
|
||||||
is PermalinkData.UserLink -> permalinkData.toMatrixItem()
|
is PermalinkData.UserLink -> permalinkData.toMatrixItem()
|
||||||
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
|
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
|
||||||
else -> null
|
else -> null
|
||||||
} ?: return null
|
} ?: return null
|
||||||
return createPillImageSpan(matrixItem)
|
return createPillImageSpan(matrixItem)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun PermalinkData.UserLink.toMatrixItem(): MatrixItem? =
|
private fun PermalinkData.UserLink.toMatrixItem(): MatrixItem? =
|
||||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.app.features.permalink
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
@ -38,7 +37,6 @@ import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
||||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
|
||||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
|
||||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
@ -70,10 +68,11 @@ class PermalinkHandler @Inject constructor(
|
||||||
buildTask: Boolean = false,
|
buildTask: Boolean = false,
|
||||||
openAnonymously: Boolean = false
|
openAnonymously: Boolean = false
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
val supportedHosts = fragmentActivity.resources.getStringArray(R.array.permalink_supported_hosts)
|
||||||
return when {
|
return when {
|
||||||
deepLink == null -> false
|
deepLink == null -> false
|
||||||
deepLink.isIgnored() -> true
|
deepLink.isIgnored() -> true
|
||||||
!isPermalinkSupported(fragmentActivity, deepLink.toString()) -> false
|
!activeSessionHolder.getSafeActiveSession()?.permalinkService()?.isPermalinkSupported(supportedHosts, deepLink.toString()).orFalse() -> false
|
||||||
else -> {
|
else -> {
|
||||||
tryOrNull {
|
tryOrNull {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
|
@ -172,12 +171,6 @@ class PermalinkHandler @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isPermalinkSupported(context: Context, url: String): Boolean {
|
|
||||||
return url.startsWith(PermalinkService.MATRIX_TO_URL_BASE) ||
|
|
||||||
context.resources.getStringArray(R.array.permalink_supported_hosts)
|
|
||||||
.any { url.toUri().host == it }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun PermalinkData.RoomLink.getRoomId(): String? {
|
private suspend fun PermalinkData.RoomLink.getRoomId(): String? {
|
||||||
val session = activeSessionHolder.getSafeActiveSession()
|
val session = activeSessionHolder.getSafeActiveSession()
|
||||||
return if (isRoomAlias && session != null) {
|
return if (isRoomAlias && session != null) {
|
||||||
|
|
|
@ -527,7 +527,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
State.Idle -> {
|
State.Idle -> {
|
||||||
// restart the playback time if player completed with less than 1s remaining time
|
// restart the playback time if player completed with less than 1s remaining time
|
||||||
if (percentage == null || (playlist.duration - position) < 1000) {
|
if (percentage == null || (playlist.duration - position) < 1000) {
|
||||||
playbackTracker.stopPlayback(id)
|
playbackTracker.stopPlaybackOrRecorder(id)
|
||||||
} else {
|
} else {
|
||||||
playbackTracker.updatePausedAtPlaybackTime(id, position, percentage)
|
playbackTracker.updatePausedAtPlaybackTime(id, position, percentage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import im.vector.app.core.resources.BuildMeta
|
import im.vector.app.core.resources.BuildMeta
|
||||||
import im.vector.app.features.attachments.toContentAttachmentData
|
import im.vector.app.features.attachments.toContentAttachmentData
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
||||||
|
@ -54,6 +55,7 @@ import javax.inject.Inject
|
||||||
class StartVoiceBroadcastUseCase @Inject constructor(
|
class StartVoiceBroadcastUseCase @Inject constructor(
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
||||||
|
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
|
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
|
||||||
|
@ -106,10 +108,14 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
|
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
|
||||||
if (state == VoiceBroadcastRecorder.State.Error) {
|
when (state) {
|
||||||
session.coroutineScope.launch {
|
VoiceBroadcastRecorder.State.Recording -> playbackTracker.updateCurrentRecording(AudioMessagePlaybackTracker.RECORDING_ID, emptyList())
|
||||||
pauseVoiceBroadcastUseCase.execute(room.roomId)
|
VoiceBroadcastRecorder.State.Idle -> playbackTracker.stopPlaybackOrRecorder(AudioMessagePlaybackTracker.RECORDING_ID)
|
||||||
|
VoiceBroadcastRecorder.State.Error -> {
|
||||||
|
playbackTracker.stopPlaybackOrRecorder(AudioMessagePlaybackTracker.RECORDING_ID)
|
||||||
|
session.coroutineScope.launch { pauseVoiceBroadcastUseCase.execute(room.roomId) }
|
||||||
}
|
}
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -47,8 +47,7 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
|
fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
|
||||||
val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
|
return getMostRecentVoiceBroadcastEventFlow(voiceBroadcast)
|
||||||
return getMostRecentVoiceBroadcastEventFlow(room, voiceBroadcast)
|
|
||||||
.onEach { event ->
|
.onEach { event ->
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"## VoiceBroadcast | " +
|
"## VoiceBroadcast | " +
|
||||||
|
@ -61,7 +60,8 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
|
||||||
/**
|
/**
|
||||||
* Get a flow of the most recent event for the given voice broadcast.
|
* Get a flow of the most recent event for the given voice broadcast.
|
||||||
*/
|
*/
|
||||||
private fun getMostRecentVoiceBroadcastEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
|
private fun getMostRecentVoiceBroadcastEventFlow(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
|
||||||
|
val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
|
||||||
val startedEventFlow = room.flow().liveTimelineEvent(voiceBroadcast.voiceBroadcastId)
|
val startedEventFlow = room.flow().liveTimelineEvent(voiceBroadcast.voiceBroadcastId)
|
||||||
// observe started event changes
|
// observe started event changes
|
||||||
return startedEventFlow
|
return startedEventFlow
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -57,6 +57,7 @@ class StartVoiceBroadcastUseCaseTest {
|
||||||
StartVoiceBroadcastUseCase(
|
StartVoiceBroadcastUseCase(
|
||||||
session = fakeSession,
|
session = fakeSession,
|
||||||
voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
|
voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
|
||||||
|
playbackTracker = mockk(),
|
||||||
context = FakeContext().instance,
|
context = FakeContext().instance,
|
||||||
buildMeta = mockk(),
|
buildMeta = mockk(),
|
||||||
getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase,
|
getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase,
|
||||||
|
|
Loading…
Reference in New Issue