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
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter {
|
|||
withScreenshot: Boolean,
|
||||
problemDescription: String,
|
||||
canContact: Boolean,
|
||||
sendPushRules: Boolean,
|
||||
listener: BugReporterListener,
|
||||
) {
|
||||
delay(100)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue