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