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:
SpiritCroc 2023-04-20 10:50:35 +02:00
commit acdf419768
33 changed files with 227 additions and 105 deletions

View File

@ -11,7 +11,7 @@ jobs:
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
uses: danger/danger-js@11.2.4
uses: danger/danger-js@11.2.6
with:
args: "--dangerfile ./tools/danger/dangerfile.js"
env:

View File

@ -66,7 +66,7 @@ jobs:
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
uses: danger/danger-js@11.2.4
uses: danger/danger-js@11.2.6
with:
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env:

View File

@ -23,7 +23,7 @@ jobs:
- name: Run Emoji script
run: ./tools/import_emojis.py
- name: Create Pull Request for Emojis
uses: peter-evans/create-pull-request@v4
uses: peter-evans/create-pull-request@v5
with:
commit-message: Sync Emojis
title: Sync Emojis
@ -49,7 +49,7 @@ jobs:
- name: Run SAS String script
run: ./tools/import_sas_strings.py
- name: Create Pull Request for SAS Strings
uses: peter-evans/create-pull-request@v4
uses: peter-evans/create-pull-request@v5
with:
commit-message: Sync SAS Strings
title: Sync SAS Strings
@ -68,7 +68,7 @@ jobs:
- name: Run analytics import script
run: ./tools/import_analytic_plan.sh
- name: Create Pull Request for analytics plan
uses: peter-evans/create-pull-request@v4
uses: peter-evans/create-pull-request@v5
with:
commit-message: Sync analytics plan
title: Sync analytics plan

View File

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

View File

@ -18,7 +18,7 @@ def markwon = "4.6.2"
def moshi = "1.14.0"
def lifecycle = "2.5.1"
def flowBinding = "1.2.0"
def flipper = "0.188.0"
def flipper = "0.189.0"
def epoxy = "5.0.0"
def mavericks = "3.0.2"
def glide = "4.14.2"
@ -27,7 +27,7 @@ def jjwt = "0.11.5"
def vanniktechEmoji = "0.16.0"
def sentry = "6.17.0"
// Use 1.6.0 alpha to fix issue with test
def fragment = "1.6.0-alpha08"
def fragment = "1.6.0-alpha09"
// 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 espresso = "3.5.1"
@ -51,14 +51,14 @@ ext.libs = [
'activity' : "androidx.activity:activity-ktx:1.7.0",
'appCompat' : "androidx.appcompat:appcompat:1.6.1",
'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",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.6",
'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment",
'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment",
'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment",
'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",
'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0",
'junit' : "androidx.test.ext:junit:1.1.5",
@ -87,7 +87,7 @@ ext.libs = [
//'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
//'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution",
// 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' : "com.google.dagger:dagger:$dagger",

View File

@ -0,0 +1,2 @@
Main changes in this version: Mainly bugfixing.
Full changelog: https://github.com/vector-im/element-android/releases

View File

@ -62,7 +62,7 @@ android {
// that the app's state is completely cleared between tests.
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_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

View File

@ -48,4 +48,8 @@ class TestPermalinkService : PermalinkService {
MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
}
}
override fun isPermalinkSupported(supportedHosts: Array<String>, url: String): Boolean {
return false
}
}

View File

@ -62,7 +62,7 @@ object MatrixPatterns {
// regex pattern to find permalink with message id.
// Android does not support in URL so extract it.
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 = "/"
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK = PERMALINK_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE)

View File

@ -97,4 +97,15 @@ interface PermalinkService {
* @return the created template
*/
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
}

View File

@ -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.toModel
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_OR_NULL
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
@ -80,7 +81,7 @@ internal fun ChunkEntity.addTimelineEvent(
val senderId = eventEntity.sender ?: ""
// 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 {
this.localId = localId
this.root = eventEntity
@ -130,7 +131,7 @@ internal fun computeIsUnique(
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()
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {
this.roomId = roomId
@ -140,12 +141,20 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE
val timestampOfEvent = originServerTs.toDouble()
// SC: fight duplicate read receipts in main timeline
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)
} else {
setOf(eventEntity.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 shouldSkipMon: Boolean
if (rootThreadEventId == THREAD_ID_MAIN_OR_NULL) {

View File

@ -16,6 +16,7 @@
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.permalinks.PermalinkService
import javax.inject.Inject
@ -47,4 +48,9 @@ internal class DefaultPermalinkService @Inject constructor(
override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
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 }
}
}

View File

@ -2599,7 +2599,9 @@
"face",
"shaking",
"shock",
"vibrate"
"vibrate",
"dizzy",
"blurry"
]
},
"relieved-face": {
@ -3800,7 +3802,8 @@
"heart",
"like",
"love",
"pink"
"pink",
"valentines"
]
},
"orange-heart": {
@ -3854,8 +3857,9 @@
"cyan",
"heart",
"light blue",
"light blue heart",
"teal"
"teal",
"ice",
"baby blue"
]
},
"purple-heart": {
@ -3892,10 +3896,10 @@
"b": "1FA76",
"j": [
"gray",
"grey heart",
"heart",
"silver",
"slate"
"slate",
"monochrome"
]
},
"white-heart": {
@ -4198,11 +4202,12 @@
"j": [
"high five",
"leftward",
"leftwards pushing hand",
"push",
"refuse",
"stop",
"wait"
"wait",
"highfive",
"pressing"
]
},
"rightwards-pushing-hand": {
@ -4213,9 +4218,10 @@
"push",
"refuse",
"rightward",
"rightwards pushing hand",
"stop",
"wait"
"wait",
"highfive",
"pressing"
]
},
"ok-hand": {
@ -8620,7 +8626,11 @@
"antlers",
"elk",
"mammal",
"moose"
"shrek",
"canada",
"sweden",
"sven",
"cool"
]
},
"donkey": {
@ -8630,10 +8640,10 @@
"animal",
"ass",
"burro",
"donkey",
"mammal",
"mule",
"stubborn"
"stubborn",
"eeyore"
]
},
"horse": {
@ -9311,7 +9321,8 @@
"bird",
"flying",
"mythology",
"wing"
"angel",
"birds"
]
},
"black-bird": {
@ -9331,9 +9342,10 @@
"j": [
"bird",
"fowl",
"goose",
"honk",
"silly"
"silly",
"jemima",
"goosebumps"
]
},
"frog": {
@ -9581,10 +9593,11 @@
"burn",
"invertebrate",
"jelly",
"jellyfish",
"marine",
"ouch",
"stinger"
"stinger",
"sting",
"tentacles"
]
},
"snail": {
@ -9885,7 +9898,6 @@
"j": [
"bluebonnet",
"flower",
"hyacinth",
"lavender",
"lupine",
"snapdragon"
@ -10423,9 +10435,11 @@
"b": "1FADA",
"j": [
"beer",
"ginger root",
"root",
"spice"
"spice",
"yellow",
"cooking",
"gingerbread"
]
},
"pea-pod": {
@ -10437,7 +10451,9 @@
"legume",
"pea",
"pod",
"vegetable"
"vegetable",
"cozy",
"green"
]
},
"bread": {
@ -15533,9 +15549,9 @@
"dance",
"fan",
"flutter",
"folding hand fan",
"hot",
"shy"
"shy",
"flamenco"
]
},
"purse": {
@ -15719,7 +15735,8 @@
"Afro",
"comb",
"hair",
"pick"
"pick",
"afro"
]
},
"crown": {
@ -16165,7 +16182,6 @@
"b": "1FA87",
"j": [
"instrument",
"maracas",
"music",
"percussion",
"rattle",
@ -16177,11 +16193,13 @@
"b": "1FA88",
"j": [
"fife",
"flute",
"music",
"pipe",
"recorder",
"woodwind"
"woodwind",
"bamboo",
"instrument",
"pied piper"
]
},
"mobile-phone": {
@ -19036,9 +19054,9 @@
"a": "⊛ Khanda",
"b": "1FAAF",
"j": [
"khanda",
"religion",
"Sikh"
"Sikh",
"Sikhism"
]
},
"aries": {
@ -19460,7 +19478,9 @@
"computer",
"internet",
"network",
"wireless"
"wifi",
"contactless",
"signal"
]
},
"vibration-mode": {

View File

@ -37,7 +37,7 @@ ext.versionMinor = 5
// 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
// is the value for the next regular release.
ext.versionPatch = 30
ext.versionPatch = 32
ext.scVersion = 66

View File

@ -133,7 +133,7 @@ dependencies {
implementation libs.androidx.biometric
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.moshiKt
@ -311,7 +311,7 @@ dependencies {
// 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)
//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.testRunner

View File

@ -184,6 +184,7 @@ class HomeActivityViewModel @AssistedInject constructor(
if (analyticsConfig.isEnabled) {
analyticsStore.didAskUserConsentFlow
.onEach { didAskUser ->
Timber.v("DidAskUserConsent: $didAskUser")
if (!didAskUser) {
_viewEvents.post(HomeActivityViewEvents.ShowAnalyticsOptIn)
} else {

View File

@ -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.voice.VoiceRecorderFragment
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.action.EventSharedAction
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.TimelineReadMarkerItem
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.url.PreviewUrlRetriever
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet

View File

@ -1442,13 +1442,17 @@ class TimelineViewModel @AssistedInject constructor(
computeUnreadState(timelineEvents, roomSummary)
}
// 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.
.distinctUntilChanged { previous, current ->
when {
previous is UnreadState.Unknown || previous is UnreadState.ReadMarkerNotLoaded -> false
previous is UnreadState.HasUnread && current is UnreadState.HasUnread &&
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
else -> false
}

View File

@ -239,7 +239,7 @@ class AudioMessageHelper @Inject constructor(
val percentage = currentPosition.toFloat() / totalDuration
playbackTracker.updatePlayingAtPlaybackTime(id, currentPosition, percentage)
} else {
playbackTracker.stopPlayback(id)
playbackTracker.stopPlaybackOrRecorder(id)
stopPlaybackTicker()
}
}

View File

@ -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.ReadReceiptData
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.url.PreviewUrlRetriever
import im.vector.app.features.media.AttachmentData
@ -213,7 +215,7 @@ class TimelineEventController @Inject constructor(
// Map eventId to adapter position
private val adapterPositionMapping = HashMap<String, Int>()
private val timelineEventsGroups = TimelineEventsGroups()
private val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
private val readReceiptsCache = ReadReceiptsCache()
private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList()
private var inSubmitList: Boolean = false
@ -463,7 +465,7 @@ class TimelineEventController @Inject constructor(
}
Timber.v("Preprocess events took $preprocessEventsTiming ms")
var numberOfEventsToBuild = 0
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvent)
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(readReceiptsCache.receiptsByEvent())
(0 until modelCache.size).forEach { position ->
val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position)
@ -509,7 +511,7 @@ class TimelineEventController @Inject constructor(
}
val itemCachedData = modelCache[position] ?: return@forEach
// 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")
}
@ -598,15 +600,15 @@ class TimelineEventController @Inject constructor(
}
private fun preprocessReverseEvents() {
receiptsByEvent.clear()
readReceiptsCache.clear()
timelineEventsGroups.clear()
val itr = currentSnapshot.listIterator(currentSnapshot.size)
var lastShownEventId: String? = null
while (itr.hasPrevious()) {
val event = itr.previous()
timelineEventsGroups.addOrIgnore(event)
val currentReadReceipts = ArrayList(event.readReceipts).filter {
it.roomMember.userId != session.myUserId && it.isVisibleInThisThread()
val currentReadReceipts = event.readReceipts.filter {
it.roomMember.userId != session.myUserId
}
if (timelineEventVisibilityHelper.shouldShowEvent(
timelineEvent = event,
@ -619,18 +621,7 @@ class TimelineEventController @Inject constructor(
if (lastShownEventId == null) {
continue
}
val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() }
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
readReceiptsCache.addReceiptsOnEvent(currentReadReceipts, lastShownEventId)
}
}

View File

@ -100,7 +100,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
}
}
fun stopPlayback(id: String) {
fun stopPlaybackOrRecorder(id: String) {
val state = getPlaybackState(id)
if (state !is Listener.State.Error) {
setState(id, Listener.State.Idle)

View File

@ -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");
* you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* 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.TextView

View File

@ -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");
* you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* 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.Parcelable

View File

@ -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");
* you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* 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 de.spiritcroc.matrixsdk.util.DbgUtil

View File

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

View File

@ -29,7 +29,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer
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.getUserOrDefault
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
@ -99,7 +99,9 @@ class EventTextRenderer @AssistedInject constructor(
private fun addPermalinksSpans(text: Spannable) {
for (match in Patterns.WEB_URL.toRegex().findAll(text)) {
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)) {
is PermalinkData.UserLink -> permalinkData.toMatrixItem()
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()

View File

@ -22,11 +22,13 @@ import android.text.Spanned
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer
import io.noties.markwon.core.spans.LinkSpan
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.getUser
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
@ -106,12 +108,18 @@ class PillsPostProcessor @AssistedInject constructor(
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
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)) {
is PermalinkData.UserLink -> permalinkData.toMatrixItem()
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
else -> null
} ?: return null
return createPillImageSpan(matrixItem)
} else {
return null
}
}
private fun PermalinkData.UserLink.toMatrixItem(): MatrixItem? =

View File

@ -18,7 +18,6 @@ package im.vector.app.features.permalink
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import im.vector.app.R
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.permalinks.PermalinkData
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.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -70,10 +68,11 @@ class PermalinkHandler @Inject constructor(
buildTask: Boolean = false,
openAnonymously: Boolean = false
): Boolean {
val supportedHosts = fragmentActivity.resources.getStringArray(R.array.permalink_supported_hosts)
return when {
deepLink == null -> false
deepLink.isIgnored() -> true
!isPermalinkSupported(fragmentActivity, deepLink.toString()) -> false
!activeSessionHolder.getSafeActiveSession()?.permalinkService()?.isPermalinkSupported(supportedHosts, deepLink.toString()).orFalse() -> false
else -> {
tryOrNull {
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? {
val session = activeSessionHolder.getSafeActiveSession()
return if (isRoomAlias && session != null) {

View File

@ -527,7 +527,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
State.Idle -> {
// restart the playback time if player completed with less than 1s remaining time
if (percentage == null || (playlist.duration - position) < 1000) {
playbackTracker.stopPlayback(id)
playbackTracker.stopPlaybackOrRecorder(id)
} else {
playbackTracker.updatePausedAtPlaybackTime(id, position, percentage)
}

View File

@ -20,6 +20,7 @@ import android.content.Context
import androidx.core.content.FileProvider
import im.vector.app.core.resources.BuildMeta
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.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
@ -54,6 +55,7 @@ import javax.inject.Inject
class StartVoiceBroadcastUseCase @Inject constructor(
private val session: Session,
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
private val playbackTracker: AudioMessagePlaybackTracker,
private val context: Context,
private val buildMeta: BuildMeta,
private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase,
@ -106,10 +108,14 @@ class StartVoiceBroadcastUseCase @Inject constructor(
}
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
if (state == VoiceBroadcastRecorder.State.Error) {
session.coroutineScope.launch {
pauseVoiceBroadcastUseCase.execute(room.roomId)
when (state) {
VoiceBroadcastRecorder.State.Recording -> playbackTracker.updateCurrentRecording(AudioMessagePlaybackTracker.RECORDING_ID, emptyList())
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
}
}
})

View File

@ -47,8 +47,7 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
) {
fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
return getMostRecentVoiceBroadcastEventFlow(room, voiceBroadcast)
return getMostRecentVoiceBroadcastEventFlow(voiceBroadcast)
.onEach { event ->
Timber.d(
"## VoiceBroadcast | " +
@ -61,7 +60,8 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor(
/**
* 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)
// observe started event changes
return startedEventFlow

File diff suppressed because one or more lines are too long

View File

@ -57,6 +57,7 @@ class StartVoiceBroadcastUseCaseTest {
StartVoiceBroadcastUseCase(
session = fakeSession,
voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
playbackTracker = mockk(),
context = FakeContext().instance,
buildMeta = mockk(),
getRoomLiveVoiceBroadcastsUseCase = fakeGetRoomLiveVoiceBroadcastsUseCase,