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
|
|
@ -35,6 +35,7 @@ import kotlinx.coroutines.CoroutineName
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@BindingContainer
|
@BindingContainer
|
||||||
|
|
@ -120,4 +121,10 @@ object AppModule {
|
||||||
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
|
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
|
||||||
return DefaultEmojibaseProvider(context)
|
return DefaultEmojibaseProvider(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
|
fun providesJson(): Json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ class CallScreenPresenter(
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
@AppCoroutineScope
|
@AppCoroutineScope
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
|
private val widgetMessageSerializer: WidgetMessageSerializer,
|
||||||
) : Presenter<CallScreenState> {
|
) : Presenter<CallScreenState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -258,7 +259,7 @@ class CallScreenPresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseMessage(message: String): WidgetMessage? {
|
private fun parseMessage(message: String): WidgetMessage? {
|
||||||
return WidgetMessageSerializer.deserialize(message).getOrNull()
|
return widgetMessageSerializer.deserialize(message).getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendHangupMessage(widgetId: String, messageInterceptor: WidgetMessageInterceptor) {
|
private fun sendHangupMessage(widgetId: String, messageInterceptor: WidgetMessageInterceptor) {
|
||||||
|
|
@ -269,7 +270,7 @@ class CallScreenPresenter(
|
||||||
action = WidgetMessage.Action.HangUp,
|
action = WidgetMessage.Action.HangUp,
|
||||||
data = null,
|
data = null,
|
||||||
)
|
)
|
||||||
messageInterceptor.sendMessage(WidgetMessageSerializer.serialize(message))
|
messageInterceptor.sendMessage(widgetMessageSerializer.serialize(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.close(widgetDriver: MatrixWidgetDriver?, navigator: CallScreenNavigator) = launch(dispatchers.io) {
|
private fun CoroutineScope.close(widgetDriver: MatrixWidgetDriver?, navigator: CallScreenNavigator) = launch(dispatchers.io) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
package io.element.android.features.call.impl.ui
|
package io.element.android.features.call.impl.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.util.Log
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.webkit.ConsoleMessage
|
import android.webkit.ConsoleMessage
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
|
|
@ -60,6 +59,7 @@ interface CallScreenNavigator {
|
||||||
internal fun CallScreenView(
|
internal fun CallScreenView(
|
||||||
state: CallScreenState,
|
state: CallScreenState,
|
||||||
pipState: PictureInPictureState,
|
pipState: PictureInPictureState,
|
||||||
|
onConsoleMessage: (ConsoleMessage) -> Unit,
|
||||||
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -108,6 +108,7 @@ internal fun CallScreenView(
|
||||||
val callback: RequestPermissionCallback = { request.grant(it) }
|
val callback: RequestPermissionCallback = { request.grant(it) }
|
||||||
requestPermissions(androidPermissions.toTypedArray(), callback)
|
requestPermissions(androidPermissions.toTypedArray(), callback)
|
||||||
},
|
},
|
||||||
|
onConsoleMessage = onConsoleMessage,
|
||||||
onCreateWebView = { webView ->
|
onCreateWebView = { webView ->
|
||||||
webView.addBackHandler(onBackPressed = ::handleBack)
|
webView.addBackHandler(onBackPressed = ::handleBack)
|
||||||
val interceptor = WebViewWidgetMessageInterceptor(
|
val interceptor = WebViewWidgetMessageInterceptor(
|
||||||
|
|
@ -174,6 +175,7 @@ private fun CallWebView(
|
||||||
url: AsyncData<String>,
|
url: AsyncData<String>,
|
||||||
userAgent: String,
|
userAgent: String,
|
||||||
onPermissionsRequest: (PermissionRequest) -> Unit,
|
onPermissionsRequest: (PermissionRequest) -> Unit,
|
||||||
|
onConsoleMessage: (ConsoleMessage) -> Unit,
|
||||||
onCreateWebView: (WebView) -> Unit,
|
onCreateWebView: (WebView) -> Unit,
|
||||||
onDestroyWebView: (WebView) -> Unit,
|
onDestroyWebView: (WebView) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -188,7 +190,11 @@ private fun CallWebView(
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
WebView(context).apply {
|
WebView(context).apply {
|
||||||
onCreateWebView(this)
|
onCreateWebView(this)
|
||||||
setup(userAgent, onPermissionsRequest)
|
setup(
|
||||||
|
userAgent = userAgent,
|
||||||
|
onPermissionsRequested = onPermissionsRequest,
|
||||||
|
onConsoleMessage = onConsoleMessage,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { webView ->
|
update = { webView ->
|
||||||
|
|
@ -208,6 +214,7 @@ private fun CallWebView(
|
||||||
private fun WebView.setup(
|
private fun WebView.setup(
|
||||||
userAgent: String,
|
userAgent: String,
|
||||||
onPermissionsRequested: (PermissionRequest) -> Unit,
|
onPermissionsRequested: (PermissionRequest) -> Unit,
|
||||||
|
onConsoleMessage: (ConsoleMessage) -> Unit,
|
||||||
) {
|
) {
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
|
@ -232,35 +239,7 @@ private fun WebView.setup(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
||||||
val priority = when (consoleMessage.messageLevel()) {
|
onConsoleMessage(consoleMessage)
|
||||||
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())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,6 +265,7 @@ internal fun CallScreenViewPreview(
|
||||||
state = state,
|
state = state,
|
||||||
pipState = aPictureInPictureState(),
|
pipState = aPictureInPictureState(),
|
||||||
requestPermissions = { _, _ -> },
|
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.services.CallForegroundService
|
||||||
import io.element.android.features.call.impl.utils.CallIntentDataParser
|
import io.element.android.features.call.impl.utils.CallIntentDataParser
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
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.Presenter
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.audio.api.AudioFocus
|
import io.element.android.libraries.audio.api.AudioFocus
|
||||||
|
|
@ -65,6 +66,7 @@ class ElementCallActivity :
|
||||||
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
|
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
|
||||||
@Inject lateinit var buildMeta: BuildMeta
|
@Inject lateinit var buildMeta: BuildMeta
|
||||||
@Inject lateinit var audioFocus: AudioFocus
|
@Inject lateinit var audioFocus: AudioFocus
|
||||||
|
@Inject lateinit var consoleMessageLogger: ConsoleMessageLogger
|
||||||
|
|
||||||
private lateinit var presenter: Presenter<CallScreenState>
|
private lateinit var presenter: Presenter<CallScreenState>
|
||||||
|
|
||||||
|
|
@ -119,6 +121,9 @@ class ElementCallActivity :
|
||||||
CallScreenView(
|
CallScreenView(
|
||||||
state = state,
|
state = state,
|
||||||
pipState = pipState,
|
pipState = pipState,
|
||||||
|
onConsoleMessage = {
|
||||||
|
consoleMessageLogger.log("ElementCall", it)
|
||||||
|
},
|
||||||
requestPermissions = { permissions, callback ->
|
requestPermissions = { permissions, callback ->
|
||||||
requestPermissionCallback = callback
|
requestPermissionCallback = callback
|
||||||
requestPermissionsLauncher.launch(permissions)
|
requestPermissionsLauncher.launch(permissions)
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,20 @@
|
||||||
|
|
||||||
package io.element.android.features.call.impl.utils
|
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.features.call.impl.data.WidgetMessage
|
||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
object WidgetMessageSerializer {
|
@Inject
|
||||||
private val coder = Json { ignoreUnknownKeys = true }
|
class WidgetMessageSerializer(
|
||||||
|
private val json: Json,
|
||||||
|
) {
|
||||||
fun deserialize(message: String): Result<WidgetMessage> {
|
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 {
|
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.CallScreenEvents
|
||||||
import io.element.android.features.call.impl.ui.CallScreenNavigator
|
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.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.FakeActiveCallManager
|
||||||
import io.element.android.features.call.utils.FakeCallWidgetProvider
|
import io.element.android.features.call.utils.FakeCallWidgetProvider
|
||||||
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
|
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.advanceTimeBy
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class) class CallScreenPresenterTest {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class CallScreenPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
|
@ -409,6 +412,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
languageTagProvider = FakeLanguageTagProvider("en-US"),
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
appCoroutineScope = backgroundScope,
|
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.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
|
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
|
||||||
|
|
@ -47,6 +48,9 @@ fun HomeSpacesView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
state.spaceRooms.forEach { spaceRoom ->
|
state.spaceRooms.forEach { spaceRoom ->
|
||||||
item(spaceRoom.roomId) {
|
item(spaceRoom.roomId) {
|
||||||
val isInvitation = spaceRoom.state == CurrentUserMembership.INVITED
|
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.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.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.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
|
import io.element.android.libraries.matrix.api.room.RoomMembershipDetails
|
||||||
|
|
@ -141,11 +139,7 @@ class JoinRoomPresenter(
|
||||||
preview.previewInfo.toContentState(membershipDetails)
|
preview.previewInfo.toContentState(membershipDetails)
|
||||||
},
|
},
|
||||||
onFailure = { throwable ->
|
onFailure = { throwable ->
|
||||||
if (throwable is ClientException.MatrixApi && (throwable.kind == ErrorKind.NotFound || throwable.kind == ErrorKind.Forbidden)) {
|
ContentState.UnknownRoom
|
||||||
ContentState.UnknownRoom
|
|
||||||
} else {
|
|
||||||
ContentState.Failure(throwable)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1193,46 +1193,8 @@ class JoinRoomPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.contentState).isEqualTo(
|
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 io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
|
||||||
data class ChangeAccountProviderState(
|
data class ChangeAccountProviderState(
|
||||||
val accountProviders: ImmutableList<AccountProvider>,
|
val accountProviders: ImmutableList<AccountProvider>,
|
||||||
val canSearchForAccountProviders: Boolean,
|
val canSearchForAccountProviders: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import io.element.android.features.login.impl.login.LoginMode
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
|
||||||
data class ChooseAccountProviderState(
|
data class ChooseAccountProviderState(
|
||||||
val accountProviders: ImmutableList<AccountProvider>,
|
val accountProviders: ImmutableList<AccountProvider>,
|
||||||
val selectedAccountProvider: 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.features.login.impl.login.LoginMode
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
|
||||||
data class ConfirmAccountProviderState(
|
data class ConfirmAccountProviderState(
|
||||||
val accountProvider: AccountProvider,
|
val accountProvider: AccountProvider,
|
||||||
val isAccountCreation: Boolean,
|
val isAccountCreation: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,10 @@ interface MessageParser {
|
||||||
@Inject
|
@Inject
|
||||||
class DefaultMessageParser(
|
class DefaultMessageParser(
|
||||||
private val accountProviderDataSource: AccountProviderDataSource,
|
private val accountProviderDataSource: AccountProviderDataSource,
|
||||||
|
private val json: Json,
|
||||||
) : MessageParser {
|
) : MessageParser {
|
||||||
override fun parse(message: String): ExternalSession {
|
override fun parse(message: String): ExternalSession {
|
||||||
val parser = Json { ignoreUnknownKeys = true }
|
val response = json.decodeFromString(MobileRegistrationResponse.serializer(), message)
|
||||||
val response = parser.decodeFromString(MobileRegistrationResponse.serializer(), message)
|
|
||||||
val userId = response.userId ?: error("No user ID in response")
|
val userId = response.userId ?: error("No user ID in response")
|
||||||
val homeServer = response.homeServer ?: accountProviderDataSource.flow.value.url
|
val homeServer = response.homeServer ?: accountProviderDataSource.flow.value.url
|
||||||
val accessToken = response.accessToken ?: error("No access token in response")
|
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.features.login.impl.resolver.HomeserverData
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
|
||||||
data class SearchAccountProviderState(
|
data class SearchAccountProviderState(
|
||||||
val userInput: String,
|
val userInput: String,
|
||||||
val userInputResult: AsyncData<List<HomeserverData>>,
|
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.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
|
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.junit.Assert.assertThrows
|
import org.junit.Assert.assertThrows
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -68,7 +69,8 @@ class DefaultMessageParserTest {
|
||||||
|
|
||||||
private fun createDefaultMessageParser(): DefaultMessageParser {
|
private fun createDefaultMessageParser(): DefaultMessageParser {
|
||||||
return 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
|
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(
|
data class AnalyticsSettingsState(
|
||||||
val analyticsPreferencesState: AnalyticsPreferencesState,
|
val analyticsPreferencesState: AnalyticsPreferencesState,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ interface BugReporter {
|
||||||
* @param withScreenshot true to include the screenshot
|
* @param withScreenshot true to include the screenshot
|
||||||
* @param problemDescription the bug description
|
* @param problemDescription the bug description
|
||||||
* @param canContact true if the user opt in to be contacted directly
|
* @param canContact true if the user opt in to be contacted directly
|
||||||
|
* @param sendPushRules true to include the push rules
|
||||||
* @param listener the listener
|
* @param listener the listener
|
||||||
*/
|
*/
|
||||||
suspend fun sendBugReport(
|
suspend fun sendBugReport(
|
||||||
|
|
@ -26,6 +27,7 @@ interface BugReporter {
|
||||||
withScreenshot: Boolean,
|
withScreenshot: Boolean,
|
||||||
problemDescription: String,
|
problemDescription: String,
|
||||||
canContact: Boolean = false,
|
canContact: Boolean = false,
|
||||||
|
sendPushRules: Boolean = false,
|
||||||
listener: BugReporterListener
|
listener: BugReporterListener
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,5 @@ sealed interface BugReportEvents {
|
||||||
data class SetSendLog(val sendLog: Boolean) : BugReportEvents
|
data class SetSendLog(val sendLog: Boolean) : BugReportEvents
|
||||||
data class SetCanContact(val canContact: Boolean) : BugReportEvents
|
data class SetCanContact(val canContact: Boolean) : BugReportEvents
|
||||||
data class SetSendScreenshot(val sendScreenshot: 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) {
|
is BugReportEvents.SetSendScreenshot -> updateFormState(formState) {
|
||||||
copy(sendScreenshot = event.sendScreenshot)
|
copy(sendScreenshot = event.sendScreenshot)
|
||||||
}
|
}
|
||||||
|
is BugReportEvents.SetSendPushRules -> updateFormState(formState) {
|
||||||
|
copy(sendPushRules = event.sendPushRules)
|
||||||
|
}
|
||||||
BugReportEvents.ClearError -> {
|
BugReportEvents.ClearError -> {
|
||||||
sendingProgress.floatValue = 0f
|
sendingProgress.floatValue = 0f
|
||||||
sendingAction.value = AsyncAction.Uninitialized
|
sendingAction.value = AsyncAction.Uninitialized
|
||||||
|
|
@ -137,6 +140,7 @@ class BugReportPresenter(
|
||||||
withScreenshot = formState.sendScreenshot,
|
withScreenshot = formState.sendScreenshot,
|
||||||
problemDescription = formState.description,
|
problemDescription = formState.description,
|
||||||
canContact = formState.canContact,
|
canContact = formState.canContact,
|
||||||
|
sendPushRules = formState.sendPushRules,
|
||||||
listener = listener
|
listener = listener
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,16 @@ data class BugReportFormState(
|
||||||
val description: String,
|
val description: String,
|
||||||
val sendLogs: Boolean,
|
val sendLogs: Boolean,
|
||||||
val canContact: Boolean,
|
val canContact: Boolean,
|
||||||
val sendScreenshot: Boolean
|
val sendScreenshot: Boolean,
|
||||||
|
val sendPushRules: Boolean,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
companion object {
|
companion object {
|
||||||
val Default = BugReportFormState(
|
val Default = BugReportFormState(
|
||||||
description = "",
|
description = "",
|
||||||
sendLogs = true,
|
sendLogs = true,
|
||||||
canContact = false,
|
canContact = false,
|
||||||
sendScreenshot = false
|
sendScreenshot = false,
|
||||||
|
sendPushRules = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.features.rageshake.impl.bugreport
|
package io.element.android.features.rageshake.impl.bugreport
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
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.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
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.components.preferences.PreferenceSwitch
|
||||||
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
|
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
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.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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
|
// Submit
|
||||||
PreferenceRow {
|
PreferenceRow {
|
||||||
Button(
|
Button(
|
||||||
|
|
@ -174,9 +182,20 @@ fun BugReportView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@Preview(heightDp = 1000)
|
||||||
@Composable
|
@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(
|
BugReportView(
|
||||||
state = state,
|
state = state,
|
||||||
onSuccess = {},
|
onSuccess = {},
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
@ -113,6 +114,7 @@ class DefaultBugReporter(
|
||||||
withScreenshot: Boolean,
|
withScreenshot: Boolean,
|
||||||
problemDescription: String,
|
problemDescription: String,
|
||||||
canContact: Boolean,
|
canContact: Boolean,
|
||||||
|
sendPushRules: Boolean,
|
||||||
listener: BugReporterListener,
|
listener: BugReporterListener,
|
||||||
) {
|
) {
|
||||||
val url = bugReporterUrlProvider.provide().first()
|
val url = bugReporterUrlProvider.provide().first()
|
||||||
|
|
@ -153,6 +155,7 @@ class DefaultBugReporter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sessionData = sessionStore.getLatestSession()
|
val sessionData = sessionStore.getLatestSession()
|
||||||
|
val numberOfAccounts = sessionStore.getAllSessions().size
|
||||||
val deviceId = sessionData?.deviceId ?: "undefined"
|
val deviceId = sessionData?.deviceId ?: "undefined"
|
||||||
val userId = sessionData?.userId?.let { UserId(it) }
|
val userId = sessionData?.userId?.let { UserId(it) }
|
||||||
// build the multi part request
|
// build the multi part request
|
||||||
|
|
@ -161,6 +164,7 @@ class DefaultBugReporter(
|
||||||
.addFormDataPart("app", RageshakeConfig.BUG_REPORT_APP_NAME)
|
.addFormDataPart("app", RageshakeConfig.BUG_REPORT_APP_NAME)
|
||||||
.addFormDataPart("user_agent", userAgentProvider.provide())
|
.addFormDataPart("user_agent", userAgentProvider.provide())
|
||||||
.addFormDataPart("user_id", userId?.toString() ?: "undefined")
|
.addFormDataPart("user_id", userId?.toString() ?: "undefined")
|
||||||
|
.addFormDataPart("number_of_accounts", numberOfAccounts.toString())
|
||||||
.addFormDataPart("can_contact", canContact.toString())
|
.addFormDataPart("can_contact", canContact.toString())
|
||||||
.addFormDataPart("device_id", deviceId)
|
.addFormDataPart("device_id", deviceId)
|
||||||
.addFormDataPart("device", Build.MODEL.trim())
|
.addFormDataPart("device", Build.MODEL.trim())
|
||||||
|
|
@ -181,6 +185,16 @@ class DefaultBugReporter(
|
||||||
if (curveKey != null && edKey != null) {
|
if (curveKey != null && edKey != null) {
|
||||||
builder.addFormDataPart("device_keys", "curve25519:$curveKey, ed25519:$edKey")
|
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) {
|
if (crashCallStack.isNotEmpty() && withCrashLogs) {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<string name="screen_bug_report_include_screenshot">"Send screenshot"</string>
|
<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_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_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_send_notification_settings_title">"Send notification settings"</string>
|
||||||
<string name="screen_bug_report_view_logs">"View logs"</string>
|
<string name="screen_bug_report_view_logs">"View logs"</string>
|
||||||
</resources>
|
</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
|
@Test
|
||||||
fun `present - reset all`() = runTest {
|
fun `present - reset all`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter {
|
||||||
withScreenshot: Boolean,
|
withScreenshot: Boolean,
|
||||||
problemDescription: String,
|
problemDescription: String,
|
||||||
canContact: Boolean,
|
canContact: Boolean,
|
||||||
|
sendPushRules: Boolean,
|
||||||
listener: BugReporterListener,
|
listener: BugReporterListener,
|
||||||
) {
|
) {
|
||||||
delay(100)
|
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.auth.MatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.api.tracing.TracingService
|
import io.element.android.libraries.matrix.api.tracing.TracingService
|
||||||
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
|
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.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.test.FakeSdkMetadata
|
import io.element.android.libraries.matrix.test.FakeSdkMetadata
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
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.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
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.matrix.test.tracing.FakeTracingService
|
||||||
import io.element.android.libraries.network.useragent.DefaultUserAgentProvider
|
import io.element.android.libraries.network.useragent.DefaultUserAgentProvider
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
|
@ -65,6 +68,7 @@ class DefaultBugReporterTest {
|
||||||
withDevicesLogs = true,
|
withDevicesLogs = true,
|
||||||
withCrashLogs = true,
|
withCrashLogs = true,
|
||||||
withScreenshot = true,
|
withScreenshot = true,
|
||||||
|
sendPushRules = true,
|
||||||
problemDescription = "a bug occurred",
|
problemDescription = "a bug occurred",
|
||||||
canContact = true,
|
canContact = true,
|
||||||
listener = object : BugReporterListener {
|
listener = object : BugReporterListener {
|
||||||
|
|
@ -108,6 +112,79 @@ class DefaultBugReporterTest {
|
||||||
initialList = listOf(aSessionData(sessionId = "@foo:example.com", deviceId = "ABCDEFGH"))
|
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 fakeEncryptionService = FakeEncryptionService()
|
||||||
val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService)
|
val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService)
|
||||||
|
|
||||||
|
|
@ -147,6 +224,7 @@ class DefaultBugReporterTest {
|
||||||
assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH")
|
assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH")
|
||||||
assertThat(foundValues["sdk_sha"]).isEqualTo("123456789")
|
assertThat(foundValues["sdk_sha"]).isEqualTo("123456789")
|
||||||
assertThat(foundValues["user_id"]).isEqualTo("@foo:example.com")
|
assertThat(foundValues["user_id"]).isEqualTo("@foo:example.com")
|
||||||
|
assertThat(foundValues["number_of_accounts"]).isEqualTo("2")
|
||||||
assertThat(foundValues["text"]).isEqualTo("a bug occurred")
|
assertThat(foundValues["text"]).isEqualTo("a bug occurred")
|
||||||
assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY")
|
assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY")
|
||||||
|
|
||||||
|
|
@ -228,6 +306,7 @@ class DefaultBugReporterTest {
|
||||||
assertThat(foundValues["device_keys"]).isNull()
|
assertThat(foundValues["device_keys"]).isNull()
|
||||||
assertThat(foundValues["device_id"]).isEqualTo("undefined")
|
assertThat(foundValues["device_id"]).isEqualTo("undefined")
|
||||||
assertThat(foundValues["user_id"]).isEqualTo("undefined")
|
assertThat(foundValues["user_id"]).isEqualTo("undefined")
|
||||||
|
assertThat(foundValues["number_of_accounts"]).isEqualTo("0")
|
||||||
assertThat(foundValues["label"]).isEqualTo("crash")
|
assertThat(foundValues["label"]).isEqualTo("crash")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,6 +351,7 @@ class DefaultBugReporterTest {
|
||||||
withDevicesLogs = true,
|
withDevicesLogs = true,
|
||||||
withCrashLogs = true,
|
withCrashLogs = true,
|
||||||
withScreenshot = true,
|
withScreenshot = true,
|
||||||
|
sendPushRules = true,
|
||||||
problemDescription = "a bug occurred",
|
problemDescription = "a bug occurred",
|
||||||
canContact = true,
|
canContact = true,
|
||||||
listener = object : BugReporterListener {
|
listener = object : BugReporterListener {
|
||||||
|
|
@ -474,6 +554,6 @@ class DefaultBugReporterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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.features.securebackup.impl.setup.views.RecoveryKeyViewState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
|
||||||
data class SecureBackupEnterRecoveryKeyState(
|
data class SecureBackupEnterRecoveryKeyState(
|
||||||
val recoveryKeyViewState: RecoveryKeyViewState,
|
val recoveryKeyViewState: RecoveryKeyViewState,
|
||||||
val isSubmitEnabled: Boolean,
|
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
|
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(
|
data class SecureBackupSetupState(
|
||||||
val isChangeRecoveryKeyUserStory: Boolean,
|
val isChangeRecoveryKeyUserStory: Boolean,
|
||||||
val recoveryKeyViewState: RecoveryKeyViewState,
|
val recoveryKeyViewState: RecoveryKeyViewState,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ package io.element.android.features.signedout.impl
|
||||||
|
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||||
|
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
|
||||||
data class SignedOutState(
|
data class SignedOutState(
|
||||||
val appName: String,
|
val appName: String,
|
||||||
val signedOutSession: SessionData?,
|
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.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
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.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.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
|
@ -177,6 +178,9 @@ private fun SpaceViewContent(
|
||||||
onTopicClick = onTopicClick
|
onTopicClick = onTopicClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
state.children.forEach { spaceRoom ->
|
state.children.forEach { spaceRoom ->
|
||||||
item {
|
item {
|
||||||
|
|
|
||||||
|
|
@ -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_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||||
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
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_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
|
||||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
||||||
opusencoder = "io.element.android:opusencoder:1.2.0"
|
opusencoder = "io.element.android:opusencoder:1.2.0"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.androidutils.browser
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.ConsoleMessage
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
interface ConsoleMessageLogger {
|
||||||
|
fun log(
|
||||||
|
tag: String,
|
||||||
|
consoleMessage: ConsoleMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
@Inject
|
||||||
|
class DefaultConsoleMessageLogger : ConsoleMessageLogger {
|
||||||
|
override fun log(
|
||||||
|
tag: String,
|
||||||
|
consoleMessage: ConsoleMessage,
|
||||||
|
) {
|
||||||
|
val priority = when (consoleMessage.messageLevel()) {
|
||||||
|
ConsoleMessage.MessageLevel.ERROR -> Log.ERROR
|
||||||
|
ConsoleMessage.MessageLevel.WARNING -> Log.WARN
|
||||||
|
else -> Log.DEBUG
|
||||||
|
}
|
||||||
|
|
||||||
|
val message = buildString {
|
||||||
|
append(consoleMessage.sourceId())
|
||||||
|
append(":")
|
||||||
|
append(consoleMessage.lineNumber())
|
||||||
|
append(" ")
|
||||||
|
append(consoleMessage.message())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid logging any messages that contain "password" to prevent leaking sensitive information
|
||||||
|
if (message.contains("password=")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.tag(tag).log(
|
||||||
|
priority = priority,
|
||||||
|
message = buildString {
|
||||||
|
append(consoleMessage.sourceId())
|
||||||
|
append(":")
|
||||||
|
append(consoleMessage.lineNumber())
|
||||||
|
append(" ")
|
||||||
|
append(consoleMessage.message())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.core.bool.orFalse
|
||||||
@Suppress("ktlint:standard:property-naming")
|
@Suppress("ktlint:standard:property-naming")
|
||||||
object MimeTypes {
|
object MimeTypes {
|
||||||
const val Any: String = "*/*"
|
const val Any: String = "*/*"
|
||||||
|
const val Json = "application/json"
|
||||||
const val OctetStream = "application/octet-stream"
|
const val OctetStream = "application/octet-stream"
|
||||||
const val Apk = "application/vnd.android.package-archive"
|
const val Apk = "application/vnd.android.package-archive"
|
||||||
const val Pdf = "application/pdf"
|
const val Pdf = "application/pdf"
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,5 @@ interface NotificationSettingsService {
|
||||||
suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit>
|
suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit>
|
||||||
suspend fun getRoomsWithUserDefinedRules(): Result<List<String>>
|
suspend fun getRoomsWithUserDefinedRules(): Result<List<String>>
|
||||||
suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean>
|
suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean>
|
||||||
|
suspend fun getRawPushRules(): Result<String?>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,4 +139,8 @@ class RustNotificationSettingsService(
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
notificationSettings.await().canPushEncryptedEventToDevice()
|
notificationSettings.await().canPushEncryptedEventToDevice()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getRawPushRules(): Result<String?> = runCatchingExceptions {
|
||||||
|
notificationSettings.await().getRawPushRules()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
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.libraries.matrix.test.A_ROOM_NOTIFICATION_MODE
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
|
||||||
|
|
@ -23,6 +24,7 @@ class FakeNotificationSettingsService(
|
||||||
initialEncryptedGroupDefaultMode: RoomNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
initialEncryptedGroupDefaultMode: RoomNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||||
initialOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
initialOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
||||||
initialEncryptedOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
initialEncryptedOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
||||||
|
private val getRawPushRulesResult: () -> Result<String> = { lambdaError() },
|
||||||
) : NotificationSettingsService {
|
) : NotificationSettingsService {
|
||||||
private val notificationSettingsStateFlow = MutableStateFlow(Unit)
|
private val notificationSettingsStateFlow = MutableStateFlow(Unit)
|
||||||
private var defaultGroupRoomNotificationMode: RoomNotificationMode = initialGroupDefaultMode
|
private var defaultGroupRoomNotificationMode: RoomNotificationMode = initialGroupDefaultMode
|
||||||
|
|
@ -178,4 +180,8 @@ class FakeNotificationSettingsService(
|
||||||
fun givenCanHomeServerPushEncryptedEventsToDeviceResult(result: Result<Boolean>) {
|
fun givenCanHomeServerPushEncryptedEventsToDeviceResult(result: Result<Boolean>) {
|
||||||
canHomeServerPushEncryptedEventsToDeviceResult = result
|
canHomeServerPushEncryptedEventsToDeviceResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getRawPushRules(): Result<String?> {
|
||||||
|
return getRawPushRulesResult()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.ui.components
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
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.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
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.modifiers.onKeyboardContextMenuAction
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
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.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figma reference: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2079&m=dev
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SpaceRoomItemView(
|
fun SpaceRoomItemView(
|
||||||
spaceRoom: SpaceRoom,
|
spaceRoom: SpaceRoom,
|
||||||
|
|
@ -67,43 +72,65 @@ fun SpaceRoomItemView(
|
||||||
trailingAction: @Composable (() -> Unit)? = null,
|
trailingAction: @Composable (() -> Unit)? = null,
|
||||||
bottomAction: @Composable (() -> Unit)? = null,
|
bottomAction: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
SpaceRoomItemScaffold(
|
val clickModifier = Modifier
|
||||||
modifier = modifier,
|
.combinedClickable(
|
||||||
avatarData = spaceRoom.getAvatarData(AvatarSize.SpaceListItem),
|
onClick = onClick,
|
||||||
isSpace = spaceRoom.isSpace,
|
onLongClick = onLongClick,
|
||||||
hideAvatars = hideAvatars,
|
onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
|
||||||
heroes = spaceRoom.heroes
|
indication = ripple(),
|
||||||
.map { hero -> hero.getAvatarData(AvatarSize.SpaceListItem) }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
.toImmutableList(),
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
trailingAction = trailingAction,
|
|
||||||
) {
|
|
||||||
NameAndIndicatorRow(
|
|
||||||
name = spaceRoom.displayName,
|
|
||||||
showIndicator = showUnreadIndicator
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(1.dp))
|
.onKeyboardContextMenuAction { onLongClick }
|
||||||
SubtitleRow(
|
Box(modifier = modifier.then(clickModifier)) {
|
||||||
visibilityIcon = spaceRoom.visibilityIcon(),
|
Column(
|
||||||
subtitle = spaceRoom.subtitle()
|
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,
|
avatarData: AvatarData,
|
||||||
isSpace: Boolean,
|
isSpace: Boolean,
|
||||||
heroes: ImmutableList<AvatarData>,
|
heroes: ImmutableList<AvatarData>,
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit,
|
|
||||||
hideAvatars: Boolean,
|
hideAvatars: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
trailingAction: @Composable (() -> Unit)? = null,
|
trailingAction: @Composable (() -> Unit)? = null,
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
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(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(clickModifier)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
|
|
@ -249,7 +264,7 @@ internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class
|
||||||
hideAvatars = false,
|
hideAvatars = false,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||||
bottomAction = if (spaceRoom.state == CurrentUserMembership.INVITED) {
|
bottomAction = if (spaceRoom.state == CurrentUserMembership.INVITED) {
|
||||||
{ InviteButtonsRowMolecule({}, {}) }
|
{ InviteButtonsRowMolecule({}, {}) }
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger
|
import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger
|
||||||
import io.element.android.libraries.network.interceptors.UserAgentInterceptor
|
import io.element.android.libraries.network.interceptors.UserAgentInterceptor
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
@ -35,12 +34,6 @@ object NetworkModule {
|
||||||
addInterceptor(userAgentInterceptor)
|
addInterceptor(userAgentInterceptor)
|
||||||
if (buildMeta.isDebuggable) addInterceptor(providesHttpLoggingInterceptor())
|
if (buildMeta.isDebuggable) addInterceptor(providesHttpLoggingInterceptor())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
@Provides
|
|
||||||
@SingleIn(AppScope::class)
|
|
||||||
fun providesJson(): Json = Json {
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor {
|
private fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor {
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,14 @@ data class PushGatewayDevice(
|
||||||
* Required. The pushkey given when the pusher was created.
|
* Required. The pushkey given when the pusher was created.
|
||||||
*/
|
*/
|
||||||
@SerialName("pushkey")
|
@SerialName("pushkey")
|
||||||
val pushKey: String
|
val pushKey: String,
|
||||||
|
/** Optional. Additional pusher data. */
|
||||||
|
@SerialName("data")
|
||||||
|
val data: PusherData? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PusherData(
|
||||||
|
@SerialName("default_payload")
|
||||||
|
val defaultPayload: Map<String, String>,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,5 @@ data class PushGatewayNotification(
|
||||||
* Required. This is an array of devices that the notification should be sent to.
|
* Required. This is an array of devices that the notification should be sent to.
|
||||||
*/
|
*/
|
||||||
@SerialName("devices")
|
@SerialName("devices")
|
||||||
val devices: List<PushGatewayDevice>
|
val devices: List<PushGatewayDevice>,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,12 @@ class DefaultPushGatewayNotifyRequest(
|
||||||
devices = listOf(
|
devices = listOf(
|
||||||
PushGatewayDevice(
|
PushGatewayDevice(
|
||||||
params.appId,
|
params.appId,
|
||||||
params.pushKey
|
params.pushKey,
|
||||||
|
PusherData(mapOf(
|
||||||
|
"cs" to "A_FAKE_SECRET",
|
||||||
|
))
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@ import io.element.android.libraries.pushproviders.api.PushData
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class UnifiedPushParser {
|
class UnifiedPushParser(
|
||||||
private val json by lazy { Json { ignoreUnknownKeys = true } }
|
private val json: Json,
|
||||||
|
) {
|
||||||
fun parse(message: ByteArray, clientSecret: String): PushData? {
|
fun parse(message: ByteArray, clientSecret: String): PushData? {
|
||||||
return tryOrNull { json.decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData(clientSecret)
|
return tryOrNull { json.decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData(clientSecret)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.pushproviders.api.PushData
|
import io.element.android.libraries.pushproviders.api.PushData
|
||||||
import io.element.android.tests.testutils.assertThrowsInDebug
|
import io.element.android.tests.testutils.assertThrowsInDebug
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class UnifiedPushParserTest {
|
class UnifiedPushParserTest {
|
||||||
|
|
@ -25,7 +26,7 @@ class UnifiedPushParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test edge cases UnifiedPush`() {
|
fun `test edge cases UnifiedPush`() {
|
||||||
val pushParser = UnifiedPushParser()
|
val pushParser = createUnifiedPushParser()
|
||||||
// Empty string
|
// Empty string
|
||||||
assertThat(pushParser.parse("".toByteArray(), aClientSecret)).isNull()
|
assertThat(pushParser.parse("".toByteArray(), aClientSecret)).isNull()
|
||||||
// Empty Json
|
// Empty Json
|
||||||
|
|
@ -36,13 +37,13 @@ class UnifiedPushParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test UnifiedPush format`() {
|
fun `test UnifiedPush format`() {
|
||||||
val pushParser = UnifiedPushParser()
|
val pushParser = createUnifiedPushParser()
|
||||||
assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray(), aClientSecret)).isEqualTo(validData)
|
assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray(), aClientSecret)).isEqualTo(validData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test empty roomId`() {
|
fun `test empty roomId`() {
|
||||||
val pushParser = UnifiedPushParser()
|
val pushParser = createUnifiedPushParser()
|
||||||
assertThrowsInDebug {
|
assertThrowsInDebug {
|
||||||
pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret)
|
pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret)
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +51,7 @@ class UnifiedPushParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test invalid roomId`() {
|
fun `test invalid roomId`() {
|
||||||
val pushParser = UnifiedPushParser()
|
val pushParser = createUnifiedPushParser()
|
||||||
assertThrowsInDebug {
|
assertThrowsInDebug {
|
||||||
pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret)
|
pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret)
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +59,7 @@ class UnifiedPushParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test empty eventId`() {
|
fun `test empty eventId`() {
|
||||||
val pushParser = UnifiedPushParser()
|
val pushParser = createUnifiedPushParser()
|
||||||
assertThrowsInDebug {
|
assertThrowsInDebug {
|
||||||
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret)
|
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret)
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +67,7 @@ class UnifiedPushParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test invalid eventId`() {
|
fun `test invalid eventId`() {
|
||||||
val pushParser = UnifiedPushParser()
|
val pushParser = createUnifiedPushParser()
|
||||||
assertThrowsInDebug {
|
assertThrowsInDebug {
|
||||||
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"), aClientSecret)
|
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 {
|
private fun String.mutate(oldValue: String, newValue: String): ByteArray {
|
||||||
return replace(oldValue, newValue).toByteArray()
|
return replace(oldValue, newValue).toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createUnifiedPushParser(
|
||||||
|
json: Json = Json { ignoreUnknownKeys = true },
|
||||||
|
) = UnifiedPushParser(
|
||||||
|
json = json,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createVectorUnifiedPushMessagingReceiver(
|
private fun TestScope.createVectorUnifiedPushMessagingReceiver(
|
||||||
|
unifiedPushParser: UnifiedPushParser = createUnifiedPushParser(),
|
||||||
pushHandler: PushHandler = FakePushHandler(),
|
pushHandler: PushHandler = FakePushHandler(),
|
||||||
unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(),
|
unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(),
|
||||||
unifiedPushGatewayResolver: UnifiedPushGatewayResolver = FakeUnifiedPushGatewayResolver(),
|
unifiedPushGatewayResolver: UnifiedPushGatewayResolver = FakeUnifiedPushGatewayResolver(),
|
||||||
|
|
@ -199,7 +200,7 @@ class VectorUnifiedPushMessagingReceiverTest {
|
||||||
endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(),
|
endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(),
|
||||||
): VectorUnifiedPushMessagingReceiver {
|
): VectorUnifiedPushMessagingReceiver {
|
||||||
return VectorUnifiedPushMessagingReceiver().apply {
|
return VectorUnifiedPushMessagingReceiver().apply {
|
||||||
this.pushParser = UnifiedPushParser()
|
this.pushParser = unifiedPushParser
|
||||||
this.pushHandler = pushHandler
|
this.pushHandler = pushHandler
|
||||||
this.guardServiceStarter = NoopGuardServiceStarter()
|
this.guardServiceStarter = NoopGuardServiceStarter()
|
||||||
this.unifiedPushStore = unifiedPushStore
|
this.unifiedPushStore = unifiedPushStore
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import timber.log.Timber
|
||||||
@Inject
|
@Inject
|
||||||
class DefaultSessionWellknownRetriever(
|
class DefaultSessionWellknownRetriever(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val parser: Json,
|
private val json: Json,
|
||||||
) : SessionWellknownRetriever {
|
) : SessionWellknownRetriever {
|
||||||
private val domain by lazy { matrixClient.userIdServerName() }
|
private val domain by lazy { matrixClient.userIdServerName() }
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ class DefaultSessionWellknownRetriever(
|
||||||
.getUrl(url)
|
.getUrl(url)
|
||||||
.mapCatchingExceptions {
|
.mapCatchingExceptions {
|
||||||
val data = String(it)
|
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") }
|
.onFailure { Timber.e(it, "Failed to retrieve .well-known from $domain") }
|
||||||
.map { it.map() }
|
.map { it.map() }
|
||||||
|
|
@ -45,7 +45,7 @@ class DefaultSessionWellknownRetriever(
|
||||||
.getUrl(url)
|
.getUrl(url)
|
||||||
.mapCatchingExceptions {
|
.mapCatchingExceptions {
|
||||||
val data = String(it)
|
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") }
|
.onFailure { Timber.e(it, "Failed to retrieve Element .well-known from $domain") }
|
||||||
.map { it.map() }
|
.map { it.map() }
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,6 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
userIdServerNameLambda = { "user.domain.org" },
|
userIdServerNameLambda = { "user.domain.org" },
|
||||||
getUrlLambda = getUrlLambda,
|
getUrlLambda = getUrlLambda,
|
||||||
),
|
),
|
||||||
parser = Json { ignoreUnknownKeys = true }
|
json = Json { ignoreUnknownKeys = true },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7300d8335682f4b7b8ccbba229da035cc7146f991a4655a1f12951616871398e
|
oid sha256:40570506b75d7cb582cee70eb3d6453b673087a17a18d8ef148e3ff5edb6f1b9
|
||||||
size 88697
|
size 89228
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c1dc4637d968405a887b8db7087e3286995a0aff30c836d413992c9fff29e97a
|
oid sha256:491d0d8f7d7e269b13548c48a6eacb8fa4df5e2798aae5b3938e7eb7368ce3f9
|
||||||
size 40929
|
size 41251
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9cc67ba23c47728faf5cea8f19fd4e3815721e6b08dd1432b57886efbe12d1e2
|
oid sha256:3fb98e398abcb465b94d4138eecbccf076e229cb0807f6543d4f77ebb0353499
|
||||||
size 86926
|
size 87390
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:46eed87406db894f387b653d32214ac376bd02cabdb200efea268bb2a4ee8240
|
oid sha256:3bee6454b9fab1a579e86bece32c0d6134d08fed5a63fecf247a58d7acba142d
|
||||||
size 39837
|
size 40125
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0a0fc1b40571d634ccc59e8266861adbaa0e0764c7c625b1352b064d08fd58f2
|
oid sha256:08f1583ccded2046d10fd19890bcc70b9fedf0efe310318a6a6209bded37b423
|
||||||
size 54916
|
size 52241
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5f188d6dfb2a084f54af9c11385c227c8ef9b923e14c55713d8e5379830a50ec
|
oid sha256:e3d89f2829ddb05c63da3f4afcadf55e70a8c70b7df46d0e04eba35932fe947d
|
||||||
size 52078
|
size 49455
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3c229f0f963e247de755f45e55432191f88a51ba79baf49f0002c2c12dbdc09e
|
||||||
|
size 48613
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cbf1430985870c99c6e6a2b427257235219250e1c6805bde44de64d1382bd30a
|
||||||
|
size 113544
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5bc84691892f956b6d282ae5aff0a7ef7900aad754bb7f04e25582c25a256342
|
||||||
|
size 45892
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3c229f0f963e247de755f45e55432191f88a51ba79baf49f0002c2c12dbdc09e
|
||||||
|
size 48613
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:26a5a053b88110ed76a59a99a199ada720fd7a44ff3b95546c4993ae3d8edddf
|
||||||
|
size 37496
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c1020f77055e3369f012f73159be9e8c7bf582d4211b3b22f5409ecba46b6898
|
||||||
|
size 47180
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:47c2e74119bd6f2329f15ee3b7dbf0115c69293bd34ed5c2e67de77319b7f820
|
||||||
|
size 111436
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:984d43efa545fb9f104ea6d4eba87e768ade025eee8d1f74260049a64c7e7a7c
|
||||||
|
size 44401
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c1020f77055e3369f012f73159be9e8c7bf582d4211b3b22f5409ecba46b6898
|
||||||
|
size 47180
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:41c5eb671aaa7c8666942dbc808eb7b03f81cbde98906c5c17cdee61b1d9566b
|
||||||
|
size 35446
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:c56f65335dcc9cda18ed7f647bf1dc9428ee07762ea3d40e7d28731a81ca8f2e
|
|
||||||
size 69552
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:12d9e6e297bbb3429a4fe6dd3ac16847280e479fe89e96ec78f35159a51d01f0
|
|
||||||
size 108813
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:9b9e42889c2b31dfee71a644b2da66c0b7cf4f4f475030d7aab3a7d500e7246a
|
|
||||||
size 66366
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:c56f65335dcc9cda18ed7f647bf1dc9428ee07762ea3d40e7d28731a81ca8f2e
|
|
||||||
size 69552
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:57402ade9b70c2124b6764edbaac71d30b89c49eea8175c448cbe7520867a42f
|
|
||||||
size 49569
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:38b5fb1099f10dae154270b5df1f52f4018d355d04e0040ba9c06ebe57f259c6
|
|
||||||
size 67628
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:1a5920a102082f35d0f0a7171687998f568da5e2d68a9f9d9b5884c55c832bd2
|
|
||||||
size 106479
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5035c2ffa678e1c761030fbac56dfb3afd043c25a3df0cd6fb7c83b2b311c2ae
|
|
||||||
size 63678
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:38b5fb1099f10dae154270b5df1f52f4018d355d04e0040ba9c06ebe57f259c6
|
|
||||||
size 67628
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:946751ac246e3e833ec39da86b25d80766daed29449b1d0fcebe33c5079b724a
|
|
||||||
size 46418
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:529b87a5fd0acf0fa8dedb0d62fae5e88562fca815ab7137c51009bd32124f9c
|
oid sha256:ba78cafc20c3d865fec9c7ab92f90e2565f233b224f99fb0665ad0d0c3c2be4e
|
||||||
size 34574
|
size 34607
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0cf531dc152a18e417ac79f67c6b696721d4c870e6160f865c681b71f5de81cd
|
oid sha256:fe62bb5d157ba1b4c7a4e4f443f4b1b3e7d68bc0ac59ce7edb6fbc99e2abbdf7
|
||||||
size 34769
|
size 34795
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7387422ae0e659b9133f75307fce76d2eeed7d14c5c13c8263989c6512c87159
|
oid sha256:88db0bd5d05963761bd3b3a5a97834f8937a9a0e10723238f2c104c5d03eb81a
|
||||||
size 35056
|
size 35089
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:aaeb807d588360026877dbee0b15373ee3930da71b609bb286c1a2472c0046c0
|
oid sha256:0534ab33ee18f03d219eb5b1490d41ccd1e3a4eb6bf734f31383a75110e368b3
|
||||||
size 64629
|
size 63084
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bcc29bb9583a195023237b3078aaccb8ed50d4bd81556cfd8f2615cdbf4fb460
|
oid sha256:85971dfed5e1cb01f5b04f43cc998d9cb69b6f06a24e3ea28f32c74aa3445e94
|
||||||
size 65318
|
size 63755
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ff38ad633e2b5124c179502eb747e8fd2571160c7899f4ffde1e80a915972ad9
|
oid sha256:3e60857b4c0d801d3a6ad7f7383b8ff8428157763ca46d30c2a559a4957cb71e
|
||||||
size 59721
|
size 59706
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:74a676ff3c6dcc32a79c9efbf2e73511a95e1659b60a6c09344cd72606240f0c
|
oid sha256:53b3736b922b746a996f71aa38b3937ebc16e37eb40bb57cd1991d6f9d98ea33
|
||||||
size 34024
|
size 34022
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c983b3fd1d771d3baf38afb9d5d8ea86d13445cce34b29338f256cc97d7ed987
|
oid sha256:4f2d6b368d4a8eaa8f3a396ba6edf8252d52b0e7ebd3b3f416c60050d3cd3c57
|
||||||
size 34172
|
size 34170
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d924b5eede25be4c7854ad6852ebf55b0ea69db70b9fb0815163012f22114da3
|
oid sha256:4b0b347f48ac09d05a2348383580d26e3725af2ef48558be86541afa239f3b06
|
||||||
size 34488
|
size 34485
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:dfd10b73e17028a8d0be148902dac4e37159b3f897fbe1fd058747bc749f4231
|
oid sha256:b5e587b7c669fa8fd02b8202318cd405b6fbb3bc91928d56f4bf2ef12f3bbbcc
|
||||||
size 63438
|
size 61863
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d63f62058595f59b3557df4168e8f704ac912e6cbecbc4bbf5f8c58f3e0cb8dd
|
oid sha256:a25dcf4e97dddb032713e49e13f46836fd541382aad2a9aa9b683dbcfdd93ccc
|
||||||
size 64034
|
size 62409
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5f10de9260c6d376add33518f8703259246663aa42ca6a6f41f080f13fe921b6
|
oid sha256:f470577ca0f0de530db1ab0d531f5b14630f9082cc4c34ad4e3fcbc1ab9ee530
|
||||||
size 57970
|
size 57954
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0a7381573761bc4fe583892044618f58e0ff8aa5a94be09df577b1fd5d63fbd8
|
oid sha256:ed823d5ab8f1e0b1af536c240b2d50ba741de64d443e54d998a4e2d02e373e05
|
||||||
size 16499
|
size 16202
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8c30ba9f665b153c5cae97e54a1bedad4f24640e2333b691d713727bf09ea262
|
oid sha256:026c69cb2b201c1d753cd6197cdc124e208a3d3c9290d40e4de840ba0ac41cc0
|
||||||
size 13075
|
size 13054
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ddcf8583e7c5bc93f3d71887960d4db21a57d1b9e0422f80965ddc353fadc0cd
|
oid sha256:0da8a3a836e8b3eebb48d00fa2b5080caf80a32579d775bebd7564a525423460
|
||||||
size 8939
|
size 9019
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:113450a4368f63f9839e27d884a8e170bde2cec2fbc483376393d7717ca0cebc
|
oid sha256:a8b5298b33db0c0e74c6a786598b1826767f5ba3bcf9187846b5b9332b81a3e3
|
||||||
size 23564
|
size 23146
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:960c8e7d357885e40d8bda33cf87042091d1a42ac5010e7096edddb2e18605ce
|
oid sha256:7607784f41e0e7f6b4b1b4c983ed59aa466b01dd388a149b070a0540f6493d7f
|
||||||
size 17933
|
size 18065
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:db4c97fc50d11736276735af850d4416e32831ce69d7501442b23fc9e9799510
|
oid sha256:d234be8428b62e19b4192a17f1c9c21eeee25c7fc683f1597631b0599fc34b32
|
||||||
size 13538
|
size 13625
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bee6de2e44a69fdd6ebf0cff2b9dfb6871bbd4373d4e1fd0f7365a8af6a27960
|
oid sha256:a725468cea9dfbf210d7367277b3a4d30438caf4ace60304e9942e4509c71972
|
||||||
size 33646
|
size 34014
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1eece85ade6f98c31df6ebed9c20db694cf75f525668c8a838b1ab100364838d
|
oid sha256:3c325ae3939648b0684e3ce2105e46445359748bf5d765593fa5eaca5d7e7082
|
||||||
size 38571
|
size 38852
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:225368f5fd5948bf823d7ddaf712ed65f16170de8de76d01c3d733ed3595429b
|
oid sha256:82ea9e352708d800c28d2651220294c1a12bd09b98cda6676a14680c337f3b6e
|
||||||
size 11012
|
size 11094
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:78bd541a6e71205688c8b323812e9c2880821ea2adef232db513548b8ee05a24
|
oid sha256:85f42e6853aac517879a0d0379e655c7a23526e8e8a44f5c806323239df65639
|
||||||
size 15995
|
size 15810
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:eeae2ab8fcfc3a73b23d288f721edf0aeb23ee75fec0a1cd8b8e69b5529f11ad
|
oid sha256:288b0d6fbce8b5e90bca734635705217c01bbb2e82f516d66cd0d0cb5f854f94
|
||||||
size 12623
|
size 12568
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b3322f5cf8324a7dced0615fd76d26bef157432a7b1ab43d7e2be3760a821bed
|
oid sha256:0f931739c849cb283722149f9d89286633d7321d78b013281bb3333ae6af9cfd
|
||||||
size 9158
|
size 9122
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1084cee1b18ff76198f8c4aaf787f4891df0cdd3b5dc7728707133b1eacea544
|
oid sha256:35dcf251e3bb3613c1cf5fcc4bfed6b8bb837dd721404ac80b72d804830cd483
|
||||||
size 22667
|
size 22375
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6ca741f017239cac3b27197b72fa545945bde79d5f6216a15a474fed84431f2d
|
oid sha256:283a7f62058a6f4d29b94a7a0d35b381a3e5038cb59bf545179b807f005aa3dd
|
||||||
size 17212
|
size 17240
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:24e9202a8ae6fbbed41eab7567af08fbc2e3d294afe502b61463791f45d36220
|
oid sha256:8fb4b4a6588f44d637f770456ca621e2c47bb9e730a8d24ace74028b74056309
|
||||||
size 13131
|
size 13032
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f4ecdad099df03a2d19a86237d141e8546cfa86cbb8e03c4ace00a06950be6b6
|
oid sha256:083774bc84a79daac071eac0fca284ce49af7fb3ae21c141684fb588bd8842ed
|
||||||
size 32668
|
size 32868
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue