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

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