Merge remote-tracking branch 'origin/develop' into feature/bma/assetReader
This commit is contained in:
commit
6d779770d7
103 changed files with 475 additions and 281 deletions
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.browser
|
||||
|
||||
import android.util.Log
|
||||
import android.webkit.ConsoleMessage
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import timber.log.Timber
|
||||
|
||||
interface ConsoleMessageLogger {
|
||||
fun log(
|
||||
tag: String,
|
||||
consoleMessage: ConsoleMessage,
|
||||
)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultConsoleMessageLogger : ConsoleMessageLogger {
|
||||
override fun log(
|
||||
tag: String,
|
||||
consoleMessage: ConsoleMessage,
|
||||
) {
|
||||
val priority = when (consoleMessage.messageLevel()) {
|
||||
ConsoleMessage.MessageLevel.ERROR -> Log.ERROR
|
||||
ConsoleMessage.MessageLevel.WARNING -> Log.WARN
|
||||
else -> Log.DEBUG
|
||||
}
|
||||
|
||||
val message = buildString {
|
||||
append(consoleMessage.sourceId())
|
||||
append(":")
|
||||
append(consoleMessage.lineNumber())
|
||||
append(" ")
|
||||
append(consoleMessage.message())
|
||||
}
|
||||
|
||||
// Avoid logging any messages that contain "password" to prevent leaking sensitive information
|
||||
if (message.contains("password=")) {
|
||||
return
|
||||
}
|
||||
|
||||
Timber.tag(tag).log(
|
||||
priority = priority,
|
||||
message = buildString {
|
||||
append(consoleMessage.sourceId())
|
||||
append(":")
|
||||
append(consoleMessage.lineNumber())
|
||||
append(" ")
|
||||
append(consoleMessage.message())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.core.bool.orFalse
|
|||
@Suppress("ktlint:standard:property-naming")
|
||||
object MimeTypes {
|
||||
const val Any: String = "*/*"
|
||||
const val Json = "application/json"
|
||||
const val OctetStream = "application/octet-stream"
|
||||
const val Apk = "application/vnd.android.package-archive"
|
||||
const val Pdf = "application/pdf"
|
||||
|
|
|
|||
|
|
@ -33,4 +33,5 @@ interface NotificationSettingsService {
|
|||
suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit>
|
||||
suspend fun getRoomsWithUserDefinedRules(): Result<List<String>>
|
||||
suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean>
|
||||
suspend fun getRawPushRules(): Result<String?>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,4 +139,8 @@ class RustNotificationSettingsService(
|
|||
runCatchingExceptions {
|
||||
notificationSettings.await().canPushEncryptedEventToDevice()
|
||||
}
|
||||
|
||||
override suspend fun getRawPushRules(): Result<String?> = runCatchingExceptions {
|
||||
notificationSettings.await().getRawPushRules()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
|||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NOTIFICATION_MODE
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ class FakeNotificationSettingsService(
|
|||
initialEncryptedGroupDefaultMode: RoomNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||
initialOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
||||
initialEncryptedOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
||||
private val getRawPushRulesResult: () -> Result<String> = { lambdaError() },
|
||||
) : NotificationSettingsService {
|
||||
private val notificationSettingsStateFlow = MutableStateFlow(Unit)
|
||||
private var defaultGroupRoomNotificationMode: RoomNotificationMode = initialGroupDefaultMode
|
||||
|
|
@ -178,4 +180,8 @@ class FakeNotificationSettingsService(
|
|||
fun givenCanHomeServerPushEncryptedEventsToDeviceResult(result: Result<Boolean>) {
|
||||
canHomeServerPushEncryptedEventsToDeviceResult = result
|
||||
}
|
||||
|
||||
override suspend fun getRawPushRules(): Result<String?> {
|
||||
return getRawPushRulesResult()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.ui.components
|
|||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
|
|
@ -42,6 +43,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
|||
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
||||
|
|
@ -56,6 +58,9 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Figma reference: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2079&m=dev
|
||||
*/
|
||||
@Composable
|
||||
fun SpaceRoomItemView(
|
||||
spaceRoom: SpaceRoom,
|
||||
|
|
@ -67,43 +72,65 @@ fun SpaceRoomItemView(
|
|||
trailingAction: @Composable (() -> Unit)? = null,
|
||||
bottomAction: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
SpaceRoomItemScaffold(
|
||||
modifier = modifier,
|
||||
avatarData = spaceRoom.getAvatarData(AvatarSize.SpaceListItem),
|
||||
isSpace = spaceRoom.isSpace,
|
||||
hideAvatars = hideAvatars,
|
||||
heroes = spaceRoom.heroes
|
||||
.map { hero -> hero.getAvatarData(AvatarSize.SpaceListItem) }
|
||||
.toImmutableList(),
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
trailingAction = trailingAction,
|
||||
) {
|
||||
NameAndIndicatorRow(
|
||||
name = spaceRoom.displayName,
|
||||
showIndicator = showUnreadIndicator
|
||||
val clickModifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(1.dp))
|
||||
SubtitleRow(
|
||||
visibilityIcon = spaceRoom.visibilityIcon(),
|
||||
subtitle = spaceRoom.subtitle()
|
||||
.onKeyboardContextMenuAction { onLongClick }
|
||||
Box(modifier = modifier.then(clickModifier)) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
SpaceRoomItemScaffold(
|
||||
avatarData = spaceRoom.getAvatarData(AvatarSize.SpaceListItem),
|
||||
isSpace = spaceRoom.isSpace,
|
||||
hideAvatars = hideAvatars,
|
||||
heroes = spaceRoom.heroes
|
||||
.map { hero -> hero.getAvatarData(AvatarSize.SpaceListItem) }
|
||||
.toImmutableList(),
|
||||
trailingAction = trailingAction,
|
||||
) {
|
||||
NameAndIndicatorRow(
|
||||
name = spaceRoom.displayName,
|
||||
showIndicator = showUnreadIndicator
|
||||
)
|
||||
Spacer(modifier = Modifier.height(1.dp))
|
||||
SubtitleRow(
|
||||
visibilityIcon = spaceRoom.visibilityIcon(),
|
||||
subtitle = spaceRoom.subtitle()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(1.dp))
|
||||
val info = spaceRoom.info()
|
||||
if (info.isNotBlank()) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
text = info,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
if (bottomAction != null) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
// Match the padding of the text content (avatar + spacer)
|
||||
Box(modifier = Modifier.padding(start = AvatarSize.SpaceListItem.dp + 16.dp)) {
|
||||
bottomAction()
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
HorizontalDivider(
|
||||
modifier = Modifier
|
||||
// Match the padding of the text content (padding + avatar + spacer)
|
||||
.padding(start = AvatarSize.SpaceListItem.dp + 16.dp + 16.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(1.dp))
|
||||
val info = spaceRoom.info()
|
||||
if (info.isNotBlank()) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
text = info,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (bottomAction != null) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
bottomAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,28 +197,16 @@ private fun SpaceRoomItemScaffold(
|
|||
avatarData: AvatarData,
|
||||
isSpace: Boolean,
|
||||
heroes: ImmutableList<AvatarData>,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
hideAvatars: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
trailingAction: @Composable (() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val clickModifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
.onKeyboardContextMenuAction { onLongClick }
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(clickModifier)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
|
|
@ -249,7 +264,7 @@ internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class
|
|||
hideAvatars = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
bottomAction = if (spaceRoom.state == CurrentUserMembership.INVITED) {
|
||||
{ InviteButtonsRowMolecule({}, {}) }
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import dev.zacsweers.metro.SingleIn
|
|||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger
|
||||
import io.element.android.libraries.network.interceptors.UserAgentInterceptor
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
@ -35,12 +34,6 @@ object NetworkModule {
|
|||
addInterceptor(userAgentInterceptor)
|
||||
if (buildMeta.isDebuggable) addInterceptor(providesHttpLoggingInterceptor())
|
||||
}.build()
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesJson(): Json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor {
|
||||
|
|
|
|||
|
|
@ -21,5 +21,14 @@ data class PushGatewayDevice(
|
|||
* Required. The pushkey given when the pusher was created.
|
||||
*/
|
||||
@SerialName("pushkey")
|
||||
val pushKey: String
|
||||
val pushKey: String,
|
||||
/** Optional. Additional pusher data. */
|
||||
@SerialName("data")
|
||||
val data: PusherData? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PusherData(
|
||||
@SerialName("default_payload")
|
||||
val defaultPayload: Map<String, String>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@ data class PushGatewayNotification(
|
|||
* Required. This is an array of devices that the notification should be sent to.
|
||||
*/
|
||||
@SerialName("devices")
|
||||
val devices: List<PushGatewayDevice>
|
||||
val devices: List<PushGatewayDevice>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,9 +42,12 @@ class DefaultPushGatewayNotifyRequest(
|
|||
devices = listOf(
|
||||
PushGatewayDevice(
|
||||
params.appId,
|
||||
params.pushKey
|
||||
params.pushKey,
|
||||
PusherData(mapOf(
|
||||
"cs" to "A_FAKE_SECRET",
|
||||
))
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import io.element.android.libraries.pushproviders.api.PushData
|
|||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Inject
|
||||
class UnifiedPushParser {
|
||||
private val json by lazy { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
class UnifiedPushParser(
|
||||
private val json: Json,
|
||||
) {
|
||||
fun parse(message: ByteArray, clientSecret: String): PushData? {
|
||||
return tryOrNull { json.decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData(clientSecret)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
|||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
import io.element.android.tests.testutils.assertThrowsInDebug
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Test
|
||||
|
||||
class UnifiedPushParserTest {
|
||||
|
|
@ -25,7 +26,7 @@ class UnifiedPushParserTest {
|
|||
|
||||
@Test
|
||||
fun `test edge cases UnifiedPush`() {
|
||||
val pushParser = UnifiedPushParser()
|
||||
val pushParser = createUnifiedPushParser()
|
||||
// Empty string
|
||||
assertThat(pushParser.parse("".toByteArray(), aClientSecret)).isNull()
|
||||
// Empty Json
|
||||
|
|
@ -36,13 +37,13 @@ class UnifiedPushParserTest {
|
|||
|
||||
@Test
|
||||
fun `test UnifiedPush format`() {
|
||||
val pushParser = UnifiedPushParser()
|
||||
val pushParser = createUnifiedPushParser()
|
||||
assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray(), aClientSecret)).isEqualTo(validData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test empty roomId`() {
|
||||
val pushParser = UnifiedPushParser()
|
||||
val pushParser = createUnifiedPushParser()
|
||||
assertThrowsInDebug {
|
||||
pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret)
|
||||
}
|
||||
|
|
@ -50,7 +51,7 @@ class UnifiedPushParserTest {
|
|||
|
||||
@Test
|
||||
fun `test invalid roomId`() {
|
||||
val pushParser = UnifiedPushParser()
|
||||
val pushParser = createUnifiedPushParser()
|
||||
assertThrowsInDebug {
|
||||
pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret)
|
||||
}
|
||||
|
|
@ -58,7 +59,7 @@ class UnifiedPushParserTest {
|
|||
|
||||
@Test
|
||||
fun `test empty eventId`() {
|
||||
val pushParser = UnifiedPushParser()
|
||||
val pushParser = createUnifiedPushParser()
|
||||
assertThrowsInDebug {
|
||||
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret)
|
||||
}
|
||||
|
|
@ -66,7 +67,7 @@ class UnifiedPushParserTest {
|
|||
|
||||
@Test
|
||||
fun `test invalid eventId`() {
|
||||
val pushParser = UnifiedPushParser()
|
||||
val pushParser = createUnifiedPushParser()
|
||||
assertThrowsInDebug {
|
||||
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"), aClientSecret)
|
||||
}
|
||||
|
|
@ -81,3 +82,9 @@ class UnifiedPushParserTest {
|
|||
private fun String.mutate(oldValue: String, newValue: String): ByteArray {
|
||||
return replace(oldValue, newValue).toByteArray()
|
||||
}
|
||||
|
||||
fun createUnifiedPushParser(
|
||||
json: Json = Json { ignoreUnknownKeys = true },
|
||||
) = UnifiedPushParser(
|
||||
json = json,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
}
|
||||
|
||||
private fun TestScope.createVectorUnifiedPushMessagingReceiver(
|
||||
unifiedPushParser: UnifiedPushParser = createUnifiedPushParser(),
|
||||
pushHandler: PushHandler = FakePushHandler(),
|
||||
unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(),
|
||||
unifiedPushGatewayResolver: UnifiedPushGatewayResolver = FakeUnifiedPushGatewayResolver(),
|
||||
|
|
@ -199,7 +200,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
|||
endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(),
|
||||
): VectorUnifiedPushMessagingReceiver {
|
||||
return VectorUnifiedPushMessagingReceiver().apply {
|
||||
this.pushParser = UnifiedPushParser()
|
||||
this.pushParser = unifiedPushParser
|
||||
this.pushHandler = pushHandler
|
||||
this.guardServiceStarter = NoopGuardServiceStarter()
|
||||
this.unifiedPushStore = unifiedPushStore
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import timber.log.Timber
|
|||
@Inject
|
||||
class DefaultSessionWellknownRetriever(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val parser: Json,
|
||||
private val json: Json,
|
||||
) : SessionWellknownRetriever {
|
||||
private val domain by lazy { matrixClient.userIdServerName() }
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ class DefaultSessionWellknownRetriever(
|
|||
.getUrl(url)
|
||||
.mapCatchingExceptions {
|
||||
val data = String(it)
|
||||
parser.decodeFromString(InternalWellKnown.serializer(), data)
|
||||
json.decodeFromString(InternalWellKnown.serializer(), data)
|
||||
}
|
||||
.onFailure { Timber.e(it, "Failed to retrieve .well-known from $domain") }
|
||||
.map { it.map() }
|
||||
|
|
@ -45,7 +45,7 @@ class DefaultSessionWellknownRetriever(
|
|||
.getUrl(url)
|
||||
.mapCatchingExceptions {
|
||||
val data = String(it)
|
||||
parser.decodeFromString(InternalElementWellKnown.serializer(), data)
|
||||
json.decodeFromString(InternalElementWellKnown.serializer(), data)
|
||||
}
|
||||
.onFailure { Timber.e(it, "Failed to retrieve Element .well-known from $domain") }
|
||||
.map { it.map() }
|
||||
|
|
|
|||
|
|
@ -244,6 +244,6 @@ class DefaultSessionWellknownRetrieverTest {
|
|||
userIdServerNameLambda = { "user.domain.org" },
|
||||
getUrlLambda = getUrlLambda,
|
||||
),
|
||||
parser = Json { ignoreUnknownKeys = true }
|
||||
json = Json { ignoreUnknownKeys = true },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue