Merge remote-tracking branch 'origin/develop' into feature/bma/assetReader

This commit is contained in:
Benoit Marty 2025-10-16 20:34:38 +02:00
commit 6d779770d7
103 changed files with 475 additions and 281 deletions

View file

@ -35,6 +35,7 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.plus
import kotlinx.serialization.json.Json
import java.io.File
@BindingContainer
@ -120,4 +121,10 @@ object AppModule {
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
return DefaultEmojibaseProvider(context)
}
@Provides
@SingleIn(AppScope::class)
fun providesJson(): Json = Json {
ignoreUnknownKeys = true
}
}

View file

@ -64,6 +64,7 @@ class CallScreenPresenter(
private val appForegroundStateService: AppForegroundStateService,
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
private val widgetMessageSerializer: WidgetMessageSerializer,
) : Presenter<CallScreenState> {
@AssistedFactory
interface Factory {
@ -258,7 +259,7 @@ class CallScreenPresenter(
}
private fun parseMessage(message: String): WidgetMessage? {
return WidgetMessageSerializer.deserialize(message).getOrNull()
return widgetMessageSerializer.deserialize(message).getOrNull()
}
private fun sendHangupMessage(widgetId: String, messageInterceptor: WidgetMessageInterceptor) {
@ -269,7 +270,7 @@ class CallScreenPresenter(
action = WidgetMessage.Action.HangUp,
data = null,
)
messageInterceptor.sendMessage(WidgetMessageSerializer.serialize(message))
messageInterceptor.sendMessage(widgetMessageSerializer.serialize(message))
}
private fun CoroutineScope.close(widgetDriver: MatrixWidgetDriver?, navigator: CallScreenNavigator) = launch(dispatchers.io) {

View file

@ -8,7 +8,6 @@
package io.element.android.features.call.impl.ui
import android.annotation.SuppressLint
import android.util.Log
import android.view.ViewGroup
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
@ -60,6 +59,7 @@ interface CallScreenNavigator {
internal fun CallScreenView(
state: CallScreenState,
pipState: PictureInPictureState,
onConsoleMessage: (ConsoleMessage) -> Unit,
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
modifier: Modifier = Modifier,
) {
@ -108,6 +108,7 @@ internal fun CallScreenView(
val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback)
},
onConsoleMessage = onConsoleMessage,
onCreateWebView = { webView ->
webView.addBackHandler(onBackPressed = ::handleBack)
val interceptor = WebViewWidgetMessageInterceptor(
@ -174,6 +175,7 @@ private fun CallWebView(
url: AsyncData<String>,
userAgent: String,
onPermissionsRequest: (PermissionRequest) -> Unit,
onConsoleMessage: (ConsoleMessage) -> Unit,
onCreateWebView: (WebView) -> Unit,
onDestroyWebView: (WebView) -> Unit,
modifier: Modifier = Modifier,
@ -188,7 +190,11 @@ private fun CallWebView(
factory = { context ->
WebView(context).apply {
onCreateWebView(this)
setup(userAgent, onPermissionsRequest)
setup(
userAgent = userAgent,
onPermissionsRequested = onPermissionsRequest,
onConsoleMessage = onConsoleMessage,
)
}
},
update = { webView ->
@ -208,6 +214,7 @@ private fun CallWebView(
private fun WebView.setup(
userAgent: String,
onPermissionsRequested: (PermissionRequest) -> Unit,
onConsoleMessage: (ConsoleMessage) -> Unit,
) {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
@ -232,35 +239,7 @@ private fun WebView.setup(
}
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
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())
}
if (message.contains("password=")) {
// Avoid logging any messages that contain "password" to prevent leaking sensitive information
return true
}
Timber.tag("WebView").log(
priority = priority,
message = buildString {
append(consoleMessage.sourceId())
append(":")
append(consoleMessage.lineNumber())
append(" ")
append(consoleMessage.message())
},
)
onConsoleMessage(consoleMessage)
return true
}
}
@ -286,6 +265,7 @@ internal fun CallScreenViewPreview(
state = state,
pipState = aPictureInPictureState(),
requestPermissions = { _, _ -> },
onConsoleMessage = {},
)
}

View file

@ -42,6 +42,7 @@ import io.element.android.features.call.impl.pip.PipView
import io.element.android.features.call.impl.services.CallForegroundService
import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.audio.api.AudioFocus
@ -65,6 +66,7 @@ class ElementCallActivity :
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
@Inject lateinit var buildMeta: BuildMeta
@Inject lateinit var audioFocus: AudioFocus
@Inject lateinit var consoleMessageLogger: ConsoleMessageLogger
private lateinit var presenter: Presenter<CallScreenState>
@ -119,6 +121,9 @@ class ElementCallActivity :
CallScreenView(
state = state,
pipState = pipState,
onConsoleMessage = {
consoleMessageLogger.log("ElementCall", it)
},
requestPermissions = { permissions, callback ->
requestPermissionCallback = callback
requestPermissionsLauncher.launch(permissions)

View file

@ -7,18 +7,20 @@
package io.element.android.features.call.impl.utils
import dev.zacsweers.metro.Inject
import io.element.android.features.call.impl.data.WidgetMessage
import io.element.android.libraries.core.extensions.runCatchingExceptions
import kotlinx.serialization.json.Json
object WidgetMessageSerializer {
private val coder = Json { ignoreUnknownKeys = true }
@Inject
class WidgetMessageSerializer(
private val json: Json,
) {
fun deserialize(message: String): Result<WidgetMessage> {
return runCatchingExceptions { coder.decodeFromString(WidgetMessage.serializer(), message) }
return runCatchingExceptions { json.decodeFromString(WidgetMessage.serializer(), message) }
}
fun serialize(message: WidgetMessage): String {
return coder.encodeToString(WidgetMessage.serializer(), message)
return json.encodeToString(WidgetMessage.serializer(), message)
}
}

View file

@ -16,6 +16,7 @@ import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.ui.CallScreenEvents
import io.element.android.features.call.impl.ui.CallScreenNavigator
import io.element.android.features.call.impl.ui.CallScreenPresenter
import io.element.android.features.call.impl.utils.WidgetMessageSerializer
import io.element.android.features.call.utils.FakeActiveCallManager
import io.element.android.features.call.utils.FakeCallWidgetProvider
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
@ -46,11 +47,13 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import org.junit.Rule
import org.junit.Test
import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalCoroutinesApi::class) class CallScreenPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class)
class CallScreenPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@ -409,6 +412,7 @@ import kotlin.time.Duration.Companion.seconds
languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService,
appCoroutineScope = backgroundScope,
widgetMessageSerializer = WidgetMessageSerializer(Json { ignoreUnknownKeys = true }),
)
}
}

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
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.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
@ -47,6 +48,9 @@ fun HomeSpacesView(
)
}
}
item {
HorizontalDivider()
}
state.spaceRooms.forEach { spaceRoom ->
item(spaceRoom.roomId) {
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED

View file

@ -39,8 +39,6 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.matrix.api.exception.ErrorKind
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
@ -141,11 +139,7 @@ class JoinRoomPresenter(
preview.previewInfo.toContentState(membershipDetails)
},
onFailure = { throwable ->
if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) {
ContentState.UnknownRoom
} else {
ContentState.Failure(throwable)
}
ContentState.UnknownRoom
}
)
}

View file

@ -1193,46 +1193,8 @@ class JoinRoomPresenterTest {
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.Failure(error = AN_EXCEPTION)
ContentState.UnknownRoom
)
state.eventSink(JoinRoomEvents.RetryFetchingContent)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(ContentState.Loading)
}
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.Failure(error = AN_EXCEPTION)
)
}
}
}
@Test
fun `present - when room is not known RoomPreview is loaded with error - dismiss`() = runTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.failure(AN_EXCEPTION)
},
spaceService = FakeSpaceService(
spaceRoomListResult = { FakeSpaceRoomList() },
),
)
val presenter = createJoinRoomPresenter(
matrixClient = client
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(
ContentState.Failure(error = AN_EXCEPTION)
)
state.eventSink(JoinRoomEvents.DismissErrorAndHideContent)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(ContentState.Dismissing)
}
}
}

View file

@ -11,7 +11,6 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState
import kotlinx.collections.immutable.ImmutableList
// Do not use default value, so no member get forgotten in the presenters.
data class ChangeAccountProviderState(
val accountProviders: ImmutableList<AccountProvider>,
val canSearchForAccountProviders: Boolean,

View file

@ -12,7 +12,6 @@ import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.ImmutableList
// Do not use default value, so no member get forgotten in the presenters.
data class ChooseAccountProviderState(
val accountProviders: ImmutableList<AccountProvider>,
val selectedAccountProvider: AccountProvider?,

View file

@ -11,7 +11,6 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.login.LoginMode
import io.element.android.libraries.architecture.AsyncData
// Do not use default value, so no member get forgotten in the presenters.
data class ConfirmAccountProviderState(
val accountProvider: AccountProvider,
val isAccountCreation: Boolean,

View file

@ -26,10 +26,10 @@ interface MessageParser {
@Inject
class DefaultMessageParser(
private val accountProviderDataSource: AccountProviderDataSource,
private val json: Json,
) : MessageParser {
override fun parse(message: String): ExternalSession {
val parser = Json { ignoreUnknownKeys = true }
val response = parser.decodeFromString(MobileRegistrationResponse.serializer(), message)
val response = json.decodeFromString(MobileRegistrationResponse.serializer(), message)
val userId = response.userId ?: error("No user ID in response")
val homeServer = response.homeServer ?: accountProviderDataSource.flow.value.url
val accessToken = response.accessToken ?: error("No access token in response")

View file

@ -11,7 +11,6 @@ import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.features.login.impl.resolver.HomeserverData
import io.element.android.libraries.architecture.AsyncData
// Do not use default value, so no member get forgotten in the presenters.
data class SearchAccountProviderState(
val userInput: String,
val userInputResult: AsyncData<List<HomeserverData>>,

View file

@ -13,6 +13,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import org.junit.Assert.assertThrows
import org.junit.Test
@ -68,7 +69,8 @@ class DefaultMessageParserTest {
private fun createDefaultMessageParser(): DefaultMessageParser {
return DefaultMessageParser(
AccountProviderDataSource(FakeEnterpriseService())
accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
json = Json { ignoreUnknownKeys = true },
)
}
}

View file

@ -9,7 +9,6 @@ package io.element.android.features.preferences.impl.analytics
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
// Do not use default value, so no member get forgotten in the presenters.
data class AnalyticsSettingsState(
val analyticsPreferencesState: AnalyticsPreferencesState,
)

View file

@ -18,6 +18,7 @@ interface BugReporter {
* @param withScreenshot true to include the screenshot
* @param problemDescription the bug description
* @param canContact true if the user opt in to be contacted directly
* @param sendPushRules true to include the push rules
* @param listener the listener
*/
suspend fun sendBugReport(
@ -26,6 +27,7 @@ interface BugReporter {
withScreenshot: Boolean,
problemDescription: String,
canContact: Boolean = false,
sendPushRules: Boolean = false,
listener: BugReporterListener
)

View file

@ -16,4 +16,5 @@ sealed interface BugReportEvents {
data class SetSendLog(val sendLog: Boolean) : BugReportEvents
data class SetCanContact(val canContact: Boolean) : BugReportEvents
data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents
data class SetSendPushRules(val sendPushRules: Boolean) : BugReportEvents
}

View file

@ -105,6 +105,9 @@ class BugReportPresenter(
is BugReportEvents.SetSendScreenshot -> updateFormState(formState) {
copy(sendScreenshot = event.sendScreenshot)
}
is BugReportEvents.SetSendPushRules -> updateFormState(formState) {
copy(sendPushRules = event.sendPushRules)
}
BugReportEvents.ClearError -> {
sendingProgress.floatValue = 0f
sendingAction.value = AsyncAction.Uninitialized
@ -137,6 +140,7 @@ class BugReportPresenter(
withScreenshot = formState.sendScreenshot,
problemDescription = formState.description,
canContact = formState.canContact,
sendPushRules = formState.sendPushRules,
listener = listener
)
}

View file

@ -29,14 +29,16 @@ data class BugReportFormState(
val description: String,
val sendLogs: Boolean,
val canContact: Boolean,
val sendScreenshot: Boolean
val sendScreenshot: Boolean,
val sendPushRules: Boolean,
) : Parcelable {
companion object {
val Default = BugReportFormState(
description = "",
sendLogs = true,
canContact = false,
sendScreenshot = false
sendScreenshot = false,
sendPushRules = false,
)
}
}

View file

@ -7,6 +7,7 @@
package io.element.android.features.rageshake.impl.bugreport
import android.content.res.Configuration
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -26,6 +27,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
@ -40,7 +42,6 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
@ -142,6 +143,13 @@ fun BugReportView(
}
}
}
PreferenceSwitch(
isChecked = state.formState.sendPushRules,
onCheckedChange = { eventSink(BugReportEvents.SetSendPushRules(it)) },
enabled = isFormEnabled,
title = stringResource(R.string.screen_bug_report_send_notification_settings_title),
subtitle = stringResource(R.string.screen_bug_report_send_notification_settings_description),
)
// Submit
PreferenceRow {
Button(
@ -174,9 +182,20 @@ fun BugReportView(
}
}
@PreviewsDayNight
@Preview(heightDp = 1000)
@Composable
internal fun BugReportViewPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreview {
internal fun BugReportViewDayPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreview {
BugReportView(
state = state,
onSuccess = {},
onBackClick = {},
onViewLogs = {},
)
}
@Preview(heightDp = 1000, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
internal fun BugReportViewNightPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreview {
BugReportView(
state = state,
onSuccess = {},

View file

@ -44,6 +44,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
@ -113,6 +114,7 @@ class DefaultBugReporter(
withScreenshot: Boolean,
problemDescription: String,
canContact: Boolean,
sendPushRules: Boolean,
listener: BugReporterListener,
) {
val url = bugReporterUrlProvider.provide().first()
@ -153,6 +155,7 @@ class DefaultBugReporter(
}
}
val sessionData = sessionStore.getLatestSession()
val numberOfAccounts = sessionStore.getAllSessions().size
val deviceId = sessionData?.deviceId ?: "undefined"
val userId = sessionData?.userId?.let { UserId(it) }
// build the multi part request
@ -161,6 +164,7 @@ class DefaultBugReporter(
.addFormDataPart("app", RageshakeConfig.BUG_REPORT_APP_NAME)
.addFormDataPart("user_agent", userAgentProvider.provide())
.addFormDataPart("user_id", userId?.toString() ?: "undefined")
.addFormDataPart("number_of_accounts", numberOfAccounts.toString())
.addFormDataPart("can_contact", canContact.toString())
.addFormDataPart("device_id", deviceId)
.addFormDataPart("device", Build.MODEL.trim())
@ -181,6 +185,16 @@ class DefaultBugReporter(
if (curveKey != null && edKey != null) {
builder.addFormDataPart("device_keys", "curve25519:$curveKey, ed25519:$edKey")
}
if (sendPushRules) {
client.notificationSettingsService.getRawPushRules().getOrNull()?.let { pushRules ->
builder.addFormDataPart(
name = "file",
filename = "push_rules.json",
body = pushRules.toByteArray().toRequestBody(MimeTypes.Json.toMediaTypeOrNull())
)
}
}
}
}
if (crashCallStack.isNotEmpty() && withCrashLogs) {

View file

@ -14,7 +14,7 @@
<string name="screen_bug_report_include_screenshot">"Send screenshot"</string>
<string name="screen_bug_report_logs_description">"Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting."</string>
<string name="screen_bug_report_rash_logs_alert_title">"%1$s crashed the last time it was used. Would you like to share a crash report with us?"</string>
<string name="screen_bug_report_send_notification_settings_description">"If you are having issues with notifications, uploading the notification settings can help us pinpoint the root cause."</string>
<string name="screen_bug_report_send_notification_settings_description">"If you are having issues with notifications, uploading the notification push rules can help us pinpoint the root cause. Note these rules can contain private information, such as your display name or keywords to be notified for."</string>
<string name="screen_bug_report_send_notification_settings_title">"Send notification settings"</string>
<string name="screen_bug_report_view_logs">"View logs"</string>
</resources>

View file

@ -106,6 +106,20 @@ class BugReportPresenterTest {
}
}
@Test
fun `present - send notification settings`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SetSendPushRules(true))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendPushRules = true))
initialState.eventSink.invoke(BugReportEvents.SetSendPushRules(false))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendPushRules = false))
}
}
@Test
fun `present - reset all`() = runTest {
val presenter = createPresenter(

View file

@ -26,6 +26,7 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter {
withScreenshot: Boolean,
problemDescription: String,
canContact: Boolean,
sendPushRules: Boolean,
listener: BugReporterListener,
) {
delay(100)

View file

@ -18,12 +18,15 @@ import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.tracing.TracingService
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.FakeSdkMetadata
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.tracing.FakeTracingService
import io.element.android.libraries.network.useragent.DefaultUserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore
@ -65,6 +68,7 @@ class DefaultBugReporterTest {
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
sendPushRules = true,
problemDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {
@ -108,6 +112,79 @@ class DefaultBugReporterTest {
initialList = listOf(aSessionData(sessionId = "@foo:example.com", deviceId = "ABCDEFGH"))
)
val fakeEncryptionService = FakeEncryptionService()
val fakePushRules = "{ content: ... }"
val fakeNotificationSettingsService = FakeNotificationSettingsService(
getRawPushRulesResult = { Result.success(fakePushRules) }
)
val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService, notificationSettingsService = fakeNotificationSettingsService)
fakeEncryptionService.givenDeviceKeys("CURVECURVECURVE", "EDKEYEDKEYEDKY")
val sut = createDefaultBugReporter(
server = server,
crashDataStore = FakeCrashDataStore(),
sessionStore = mockSessionStore,
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) })
)
val progressValues = mutableListOf<Int>()
sut.sendBugReport(
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
sendPushRules = true,
problemDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {
override fun onUploadCancelled() {}
override fun onUploadFailed(reason: String?) {}
override fun onProgress(progress: Int) {
progressValues.add(progress)
}
override fun onUploadSucceed() {}
},
)
val request = server.takeRequest()
val foundValues = collectValuesFromFormData(request)
assertThat(foundValues["app"]).isEqualTo(RageshakeConfig.BUG_REPORT_APP_NAME)
assertThat(foundValues["can_contact"]).isEqualTo("true")
assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH")
assertThat(foundValues["sdk_sha"]).isEqualTo("123456789")
assertThat(foundValues["user_id"]).isEqualTo("@foo:example.com")
assertThat(foundValues["number_of_accounts"]).isEqualTo("1")
assertThat(foundValues["text"]).isEqualTo("a bug occurred")
assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY")
assertThat(foundValues["file"]).contains(fakePushRules)
// device_key now added given they are not null
// so is the push_rules value
assertThat(progressValues.size).isEqualTo(EXPECTED_NUMBER_OF_PROGRESS_VALUE + 2)
server.shutdown()
}
@Test
fun `test sendBugReport multi accounts`() = runTest {
val server = MockWebServer()
server.enqueue(
MockResponse()
.setResponseCode(200)
)
server.start()
val mockSessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = "@foo:example.com", deviceId = "ABCDEFGH"),
aSessionData(sessionId = A_USER_ID.value, deviceId = A_DEVICE_ID.value),
)
)
val fakeEncryptionService = FakeEncryptionService()
val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService)
@ -147,6 +224,7 @@ class DefaultBugReporterTest {
assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH")
assertThat(foundValues["sdk_sha"]).isEqualTo("123456789")
assertThat(foundValues["user_id"]).isEqualTo("@foo:example.com")
assertThat(foundValues["number_of_accounts"]).isEqualTo("2")
assertThat(foundValues["text"]).isEqualTo("a bug occurred")
assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY")
@ -228,6 +306,7 @@ class DefaultBugReporterTest {
assertThat(foundValues["device_keys"]).isNull()
assertThat(foundValues["device_id"]).isEqualTo("undefined")
assertThat(foundValues["user_id"]).isEqualTo("undefined")
assertThat(foundValues["number_of_accounts"]).isEqualTo("0")
assertThat(foundValues["label"]).isEqualTo("crash")
}
@ -272,6 +351,7 @@ class DefaultBugReporterTest {
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
sendPushRules = true,
problemDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {
@ -474,6 +554,6 @@ class DefaultBugReporterTest {
}
companion object {
private const val EXPECTED_NUMBER_OF_PROGRESS_VALUE = 17
private const val EXPECTED_NUMBER_OF_PROGRESS_VALUE = 18
}
}

View file

@ -10,7 +10,6 @@ package io.element.android.features.securebackup.impl.enter
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
import io.element.android.libraries.architecture.AsyncAction
// Do not use default value, so no member get forgotten in the presenters.
data class SecureBackupEnterRecoveryKeyState(
val recoveryKeyViewState: RecoveryKeyViewState,
val isSubmitEnabled: Boolean,

View file

@ -9,7 +9,6 @@ package io.element.android.features.securebackup.impl.setup
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
// Do not use default value, so no member get forgotten in the presenters.
data class SecureBackupSetupState(
val isChangeRecoveryKeyUserStory: Boolean,
val recoveryKeyViewState: RecoveryKeyViewState,

View file

@ -9,7 +9,6 @@ package io.element.android.features.signedout.impl
import io.element.android.libraries.sessionstorage.api.SessionData
// Do not use default value, so no member get forgotten in the presenters.
data class SignedOutState(
val appName: String,
val signedOutSession: SessionData?,

View file

@ -49,6 +49,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
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.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -177,6 +178,9 @@ private fun SpaceViewContent(
onTopicClick = onTopicClick
)
}
item {
HorizontalDivider()
}
}
state.children.forEach { spaceRoom ->
item {

View file

@ -198,7 +198,7 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.2"
maplibre = "org.maplibre.gl:android-sdk:12.0.0"
maplibre = "org.maplibre.gl:android-sdk:12.0.1"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.2.0"

View file

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

View file

@ -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"

View file

@ -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?>
}

View file

@ -139,4 +139,8 @@ class RustNotificationSettingsService(
runCatchingExceptions {
notificationSettings.await().canPushEncryptedEventToDevice()
}
override suspend fun getRawPushRules(): Result<String?> = runCatchingExceptions {
notificationSettings.await().getRawPushRules()
}
}

View file

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

View file

@ -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 {

View file

@ -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 {

View file

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

View file

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

View file

@ -42,9 +42,12 @@ class DefaultPushGatewayNotifyRequest(
devices = listOf(
PushGatewayDevice(
params.appId,
params.pushKey
params.pushKey,
PusherData(mapOf(
"cs" to "A_FAKE_SECRET",
))
)
)
),
)
)
)

View file

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

View file

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

View file

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

View file

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

View file

@ -244,6 +244,6 @@ class DefaultSessionWellknownRetrieverTest {
userIdServerNameLambda = { "user.domain.org" },
getUrlLambda = getUrlLambda,
),
parser = Json { ignoreUnknownKeys = true }
json = Json { ignoreUnknownKeys = true },
)
}

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7300d8335682f4b7b8ccbba229da035cc7146f991a4655a1f12951616871398e
size 88697
oid sha256:40570506b75d7cb582cee70eb3d6453b673087a17a18d8ef148e3ff5edb6f1b9
size 89228

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1dc4637d968405a887b8db7087e3286995a0aff30c836d413992c9fff29e97a
size 40929
oid sha256:491d0d8f7d7e269b13548c48a6eacb8fa4df5e2798aae5b3938e7eb7368ce3f9
size 41251

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9cc67ba23c47728faf5cea8f19fd4e3815721e6b08dd1432b57886efbe12d1e2
size 86926
oid sha256:3fb98e398abcb465b94d4138eecbccf076e229cb0807f6543d4f77ebb0353499
size 87390

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46eed87406db894f387b653d32214ac376bd02cabdb200efea268bb2a4ee8240
size 39837
oid sha256:3bee6454b9fab1a579e86bece32c0d6134d08fed5a63fecf247a58d7acba142d
size 40125

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a0fc1b40571d634ccc59e8266861adbaa0e0764c7c625b1352b064d08fd58f2
size 54916
oid sha256:08f1583ccded2046d10fd19890bcc70b9fedf0efe310318a6a6209bded37b423
size 52241

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f188d6dfb2a084f54af9c11385c227c8ef9b923e14c55713d8e5379830a50ec
size 52078
oid sha256:e3d89f2829ddb05c63da3f4afcadf55e70a8c70b7df46d0e04eba35932fe947d
size 49455

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3c229f0f963e247de755f45e55432191f88a51ba79baf49f0002c2c12dbdc09e
size 48613

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cbf1430985870c99c6e6a2b427257235219250e1c6805bde44de64d1382bd30a
size 113544

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5bc84691892f956b6d282ae5aff0a7ef7900aad754bb7f04e25582c25a256342
size 45892

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3c229f0f963e247de755f45e55432191f88a51ba79baf49f0002c2c12dbdc09e
size 48613

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:26a5a053b88110ed76a59a99a199ada720fd7a44ff3b95546c4993ae3d8edddf
size 37496

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1020f77055e3369f012f73159be9e8c7bf582d4211b3b22f5409ecba46b6898
size 47180

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:47c2e74119bd6f2329f15ee3b7dbf0115c69293bd34ed5c2e67de77319b7f820
size 111436

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:984d43efa545fb9f104ea6d4eba87e768ade025eee8d1f74260049a64c7e7a7c
size 44401

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1020f77055e3369f012f73159be9e8c7bf582d4211b3b22f5409ecba46b6898
size 47180

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41c5eb671aaa7c8666942dbc808eb7b03f81cbde98906c5c17cdee61b1d9566b
size 35446

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c56f65335dcc9cda18ed7f647bf1dc9428ee07762ea3d40e7d28731a81ca8f2e
size 69552

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:12d9e6e297bbb3429a4fe6dd3ac16847280e479fe89e96ec78f35159a51d01f0
size 108813

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9b9e42889c2b31dfee71a644b2da66c0b7cf4f4f475030d7aab3a7d500e7246a
size 66366

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c56f65335dcc9cda18ed7f647bf1dc9428ee07762ea3d40e7d28731a81ca8f2e
size 69552

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57402ade9b70c2124b6764edbaac71d30b89c49eea8175c448cbe7520867a42f
size 49569

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38b5fb1099f10dae154270b5df1f52f4018d355d04e0040ba9c06ebe57f259c6
size 67628

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a5920a102082f35d0f0a7171687998f568da5e2d68a9f9d9b5884c55c832bd2
size 106479

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5035c2ffa678e1c761030fbac56dfb3afd043c25a3df0cd6fb7c83b2b311c2ae
size 63678

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38b5fb1099f10dae154270b5df1f52f4018d355d04e0040ba9c06ebe57f259c6
size 67628

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:946751ac246e3e833ec39da86b25d80766daed29449b1d0fcebe33c5079b724a
size 46418

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:529b87a5fd0acf0fa8dedb0d62fae5e88562fca815ab7137c51009bd32124f9c
size 34574
oid sha256:ba78cafc20c3d865fec9c7ab92f90e2565f233b224f99fb0665ad0d0c3c2be4e
size 34607

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0cf531dc152a18e417ac79f67c6b696721d4c870e6160f865c681b71f5de81cd
size 34769
oid sha256:fe62bb5d157ba1b4c7a4e4f443f4b1b3e7d68bc0ac59ce7edb6fbc99e2abbdf7
size 34795

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7387422ae0e659b9133f75307fce76d2eeed7d14c5c13c8263989c6512c87159
size 35056
oid sha256:88db0bd5d05963761bd3b3a5a97834f8937a9a0e10723238f2c104c5d03eb81a
size 35089

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aaeb807d588360026877dbee0b15373ee3930da71b609bb286c1a2472c0046c0
size 64629
oid sha256:0534ab33ee18f03d219eb5b1490d41ccd1e3a4eb6bf734f31383a75110e368b3
size 63084

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bcc29bb9583a195023237b3078aaccb8ed50d4bd81556cfd8f2615cdbf4fb460
size 65318
oid sha256:85971dfed5e1cb01f5b04f43cc998d9cb69b6f06a24e3ea28f32c74aa3445e94
size 63755

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ff38ad633e2b5124c179502eb747e8fd2571160c7899f4ffde1e80a915972ad9
size 59721
oid sha256:3e60857b4c0d801d3a6ad7f7383b8ff8428157763ca46d30c2a559a4957cb71e
size 59706

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:74a676ff3c6dcc32a79c9efbf2e73511a95e1659b60a6c09344cd72606240f0c
size 34024
oid sha256:53b3736b922b746a996f71aa38b3937ebc16e37eb40bb57cd1991d6f9d98ea33
size 34022

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c983b3fd1d771d3baf38afb9d5d8ea86d13445cce34b29338f256cc97d7ed987
size 34172
oid sha256:4f2d6b368d4a8eaa8f3a396ba6edf8252d52b0e7ebd3b3f416c60050d3cd3c57
size 34170

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d924b5eede25be4c7854ad6852ebf55b0ea69db70b9fb0815163012f22114da3
size 34488
oid sha256:4b0b347f48ac09d05a2348383580d26e3725af2ef48558be86541afa239f3b06
size 34485

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dfd10b73e17028a8d0be148902dac4e37159b3f897fbe1fd058747bc749f4231
size 63438
oid sha256:b5e587b7c669fa8fd02b8202318cd405b6fbb3bc91928d56f4bf2ef12f3bbbcc
size 61863

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d63f62058595f59b3557df4168e8f704ac912e6cbecbc4bbf5f8c58f3e0cb8dd
size 64034
oid sha256:a25dcf4e97dddb032713e49e13f46836fd541382aad2a9aa9b683dbcfdd93ccc
size 62409

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f10de9260c6d376add33518f8703259246663aa42ca6a6f41f080f13fe921b6
size 57970
oid sha256:f470577ca0f0de530db1ab0d531f5b14630f9082cc4c34ad4e3fcbc1ab9ee530
size 57954

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a7381573761bc4fe583892044618f58e0ff8aa5a94be09df577b1fd5d63fbd8
size 16499
oid sha256:ed823d5ab8f1e0b1af536c240b2d50ba741de64d443e54d998a4e2d02e373e05
size 16202

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c30ba9f665b153c5cae97e54a1bedad4f24640e2333b691d713727bf09ea262
size 13075
oid sha256:026c69cb2b201c1d753cd6197cdc124e208a3d3c9290d40e4de840ba0ac41cc0
size 13054

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ddcf8583e7c5bc93f3d71887960d4db21a57d1b9e0422f80965ddc353fadc0cd
size 8939
oid sha256:0da8a3a836e8b3eebb48d00fa2b5080caf80a32579d775bebd7564a525423460
size 9019

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:113450a4368f63f9839e27d884a8e170bde2cec2fbc483376393d7717ca0cebc
size 23564
oid sha256:a8b5298b33db0c0e74c6a786598b1826767f5ba3bcf9187846b5b9332b81a3e3
size 23146

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:960c8e7d357885e40d8bda33cf87042091d1a42ac5010e7096edddb2e18605ce
size 17933
oid sha256:7607784f41e0e7f6b4b1b4c983ed59aa466b01dd388a149b070a0540f6493d7f
size 18065

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:db4c97fc50d11736276735af850d4416e32831ce69d7501442b23fc9e9799510
size 13538
oid sha256:d234be8428b62e19b4192a17f1c9c21eeee25c7fc683f1597631b0599fc34b32
size 13625

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bee6de2e44a69fdd6ebf0cff2b9dfb6871bbd4373d4e1fd0f7365a8af6a27960
size 33646
oid sha256:a725468cea9dfbf210d7367277b3a4d30438caf4ace60304e9942e4509c71972
size 34014

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1eece85ade6f98c31df6ebed9c20db694cf75f525668c8a838b1ab100364838d
size 38571
oid sha256:3c325ae3939648b0684e3ce2105e46445359748bf5d765593fa5eaca5d7e7082
size 38852

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:225368f5fd5948bf823d7ddaf712ed65f16170de8de76d01c3d733ed3595429b
size 11012
oid sha256:82ea9e352708d800c28d2651220294c1a12bd09b98cda6676a14680c337f3b6e
size 11094

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:78bd541a6e71205688c8b323812e9c2880821ea2adef232db513548b8ee05a24
size 15995
oid sha256:85f42e6853aac517879a0d0379e655c7a23526e8e8a44f5c806323239df65639
size 15810

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eeae2ab8fcfc3a73b23d288f721edf0aeb23ee75fec0a1cd8b8e69b5529f11ad
size 12623
oid sha256:288b0d6fbce8b5e90bca734635705217c01bbb2e82f516d66cd0d0cb5f854f94
size 12568

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b3322f5cf8324a7dced0615fd76d26bef157432a7b1ab43d7e2be3760a821bed
size 9158
oid sha256:0f931739c849cb283722149f9d89286633d7321d78b013281bb3333ae6af9cfd
size 9122

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1084cee1b18ff76198f8c4aaf787f4891df0cdd3b5dc7728707133b1eacea544
size 22667
oid sha256:35dcf251e3bb3613c1cf5fcc4bfed6b8bb837dd721404ac80b72d804830cd483
size 22375

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ca741f017239cac3b27197b72fa545945bde79d5f6216a15a474fed84431f2d
size 17212
oid sha256:283a7f62058a6f4d29b94a7a0d35b381a3e5038cb59bf545179b807f005aa3dd
size 17240

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:24e9202a8ae6fbbed41eab7567af08fbc2e3d294afe502b61463791f45d36220
size 13131
oid sha256:8fb4b4a6588f44d637f770456ca621e2c47bb9e730a8d24ace74028b74056309
size 13032

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f4ecdad099df03a2d19a86237d141e8546cfa86cbb8e03c4ace00a06950be6b6
size 32668
oid sha256:083774bc84a79daac071eac0fca284ce49af7fb3ae21c141684fb588bd8842ed
size 32868

Some files were not shown because too many files have changed in this diff Show more