commit
a84f387f4d
22 changed files with 87 additions and 48 deletions
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="2.1.21" />
|
<option name="version" value="2.2.0" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -98,6 +98,10 @@ allprojects {
|
||||||
|
|
||||||
// Uncomment to suppress Compose Kotlin compiler compatibility warning
|
// Uncomment to suppress Compose Kotlin compiler compatibility warning
|
||||||
// freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
|
// freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
|
||||||
|
|
||||||
|
// Fix compilation warning for annotations
|
||||||
|
// See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details
|
||||||
|
freeCompilerArgs.add("-Xannotation-default-target=first-only")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -391,7 +391,7 @@ private fun MessagesViewContent(
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberScrollBehavior(
|
val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberScrollBehavior(
|
||||||
pinnedMessagesCount = state.pinnedMessagesBannerState.pinnedMessagesCount(),
|
pinnedMessagesCount = (state.pinnedMessagesBannerState as? PinnedMessagesBannerState.Visible)?.pinnedMessagesCount() ?: 0,
|
||||||
)
|
)
|
||||||
TimelineView(
|
TimelineView(
|
||||||
state = state.timelineState,
|
state = state.timelineState,
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,31 @@ package io.element.android.features.messages.impl.pinned.banner
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import io.element.android.libraries.designsystem.text.toAnnotatedString
|
import io.element.android.libraries.designsystem.text.toAnnotatedString
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface PinnedMessagesBannerState {
|
sealed interface PinnedMessagesBannerState {
|
||||||
data object Hidden : PinnedMessagesBannerState
|
data object Hidden : PinnedMessagesBannerState
|
||||||
sealed interface Visible : PinnedMessagesBannerState
|
@Immutable
|
||||||
|
sealed interface Visible : PinnedMessagesBannerState {
|
||||||
|
fun pinnedMessagesCount() = when (this) {
|
||||||
|
is Loading -> expectedPinnedMessagesCount
|
||||||
|
is Loaded -> loadedPinnedMessagesCount
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentPinnedMessageIndex() = when (this) {
|
||||||
|
is Loading -> expectedPinnedMessagesCount - 1
|
||||||
|
is Loaded -> currentPinnedMessageIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun formattedMessage() = when (this) {
|
||||||
|
is Loading -> stringResource(id = CommonStrings.screen_room_pinned_banner_loading_description).toAnnotatedString()
|
||||||
|
is Loaded -> currentPinnedMessage.formatted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class Loading(val expectedPinnedMessagesCount: Int) : Visible
|
data class Loading(val expectedPinnedMessagesCount: Int) : Visible
|
||||||
data class Loaded(
|
data class Loaded(
|
||||||
val currentPinnedMessage: PinnedMessagesBannerItem,
|
val currentPinnedMessage: PinnedMessagesBannerItem,
|
||||||
|
|
@ -25,23 +42,4 @@ sealed interface PinnedMessagesBannerState {
|
||||||
val loadedPinnedMessagesCount: Int,
|
val loadedPinnedMessagesCount: Int,
|
||||||
val eventSink: (PinnedMessagesBannerEvents) -> Unit
|
val eventSink: (PinnedMessagesBannerEvents) -> Unit
|
||||||
) : Visible
|
) : Visible
|
||||||
|
|
||||||
fun pinnedMessagesCount() = when (this) {
|
|
||||||
is Hidden -> 0
|
|
||||||
is Loading -> expectedPinnedMessagesCount
|
|
||||||
is Loaded -> loadedPinnedMessagesCount
|
|
||||||
}
|
|
||||||
|
|
||||||
fun currentPinnedMessageIndex() = when (this) {
|
|
||||||
is Hidden -> 0
|
|
||||||
is Loading -> expectedPinnedMessagesCount - 1
|
|
||||||
is Loaded -> currentPinnedMessageIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun formattedMessage() = when (this) {
|
|
||||||
is Hidden -> AnnotatedString("")
|
|
||||||
is Loading -> stringResource(id = CommonStrings.screen_room_pinned_banner_loading_description).toAnnotatedString()
|
|
||||||
is Loaded -> currentPinnedMessage.formatted
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ fun PinnedMessagesBannerView(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PinnedMessagesBannerRow(
|
private fun PinnedMessagesBannerRow(
|
||||||
state: PinnedMessagesBannerState,
|
state: PinnedMessagesBannerState.Visible,
|
||||||
onClick: (EventId) -> Unit,
|
onClick: (EventId) -> Unit,
|
||||||
onViewAllClick: () -> Unit,
|
onViewAllClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.features.messages.impl.attachments
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
||||||
|
import io.element.android.features.messages.impl.attachments.preview.aMediaUploadInfo
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SendActionStateTest {
|
||||||
|
@Test
|
||||||
|
fun `mediaUploadInfo() should return the value from Uploading class`() {
|
||||||
|
val mediaUploadInfo: MediaUploadInfo = aMediaUploadInfo()
|
||||||
|
val state: SendActionState = SendActionState.Sending.Uploading(
|
||||||
|
progress = 0.5f,
|
||||||
|
mediaUploadInfo = aMediaUploadInfo()
|
||||||
|
)
|
||||||
|
assertThat(state.mediaUploadInfo()).isEqualTo(mediaUploadInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,7 +62,7 @@ class PinnedMessagesBannerPresenterTest {
|
||||||
val presenter = createPinnedMessagesBannerPresenter(room = room)
|
val presenter = createPinnedMessagesBannerPresenter(room = room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem() as PinnedMessagesBannerState.Loading
|
||||||
assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1))
|
assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1))
|
||||||
assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1)
|
assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1)
|
||||||
assertThat(loadingState.currentPinnedMessageIndex()).isEqualTo(0)
|
assertThat(loadingState.currentPinnedMessageIndex()).isEqualTo(0)
|
||||||
|
|
@ -167,7 +167,8 @@ class PinnedMessagesBannerPresenterTest {
|
||||||
val presenter = createPinnedMessagesBannerPresenter(room = room)
|
val presenter = createPinnedMessagesBannerPresenter(room = room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
awaitItem().also { loadingState ->
|
awaitItem().also { state ->
|
||||||
|
val loadingState = state as PinnedMessagesBannerState.Loading
|
||||||
assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1))
|
assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1))
|
||||||
assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1)
|
assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1)
|
||||||
assertThat(loadingState.currentPinnedMessageIndex()).isEqualTo(0)
|
assertThat(loadingState.currentPinnedMessageIndex()).isEqualTo(0)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ signing.element.nightly.keyPassword=Secret
|
||||||
|
|
||||||
# Customise the Lint version to use a more recent version than the one bundled with AGP
|
# Customise the Lint version to use a more recent version than the one bundled with AGP
|
||||||
# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
|
# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
|
||||||
android.experimental.lint.version=8.11.0
|
android.experimental.lint.version=8.12.0-alpha07
|
||||||
|
|
||||||
# Enable test fixture for all modules by default
|
# Enable test fixture for all modules by default
|
||||||
android.experimental.enableTestFixtures=true
|
android.experimental.enableTestFixtures=true
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
[versions]
|
[versions]
|
||||||
# Project
|
# Project
|
||||||
android_gradle_plugin = "8.11.0"
|
android_gradle_plugin = "8.11.0"
|
||||||
kotlin = "2.1.21"
|
kotlin = "2.2.0"
|
||||||
kotlinpoet = "2.2.0"
|
kotlinpoet = "2.2.0"
|
||||||
ksp = "2.1.21-2.0.2"
|
ksp = "2.2.0-2.0.2"
|
||||||
firebaseAppDistribution = "5.1.1"
|
firebaseAppDistribution = "5.1.1"
|
||||||
|
|
||||||
# AndroidX
|
# AndroidX
|
||||||
|
|
@ -33,8 +33,8 @@ accompanist = "0.37.3"
|
||||||
test_core = "1.6.1"
|
test_core = "1.6.1"
|
||||||
|
|
||||||
# Jetbrain
|
# Jetbrain
|
||||||
datetime = "0.6.2"
|
datetime = "0.7.0"
|
||||||
serialization_json = "1.8.1"
|
serialization_json = "1.9.0"
|
||||||
|
|
||||||
#other
|
#other
|
||||||
detekt = "1.23.8"
|
detekt = "1.23.8"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,14 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
optIn = listOf(
|
||||||
|
"kotlin.time.ExperimentalTime"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ class DefaultDateFormatterDay @Inject constructor(
|
||||||
return if (useRelative) {
|
return if (useRelative) {
|
||||||
val dayDiff = today.date.toEpochDays() - dateToFormat.date.toEpochDays()
|
val dayDiff = today.date.toEpochDays() - dateToFormat.date.toEpochDays()
|
||||||
when (dayDiff) {
|
when (dayDiff) {
|
||||||
0 -> dateFormatters.getRelativeDay(timestamp, "Today")
|
0L -> dateFormatters.getRelativeDay(timestamp, "Today")
|
||||||
1 -> dateFormatters.getRelativeDay(timestamp, "Yesterday")
|
1L -> dateFormatters.getRelativeDay(timestamp, "Yesterday")
|
||||||
else -> if (dayDiff < 7) {
|
else -> if (dayDiff < 7) {
|
||||||
dateFormatters.formatDateWithDay(dateToFormat)
|
dateFormatters.formatDateWithDay(dateToFormat)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ package io.element.android.libraries.dateformatter.impl
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.toInstant
|
import kotlinx.datetime.toInstant
|
||||||
import kotlinx.datetime.toJavaLocalDate
|
import kotlinx.datetime.toJavaLocalDate
|
||||||
|
|
@ -20,6 +19,7 @@ import java.time.Period
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.time.Clock
|
||||||
|
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
class DateFormatters @Inject constructor(
|
class DateFormatters @Inject constructor(
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@
|
||||||
|
|
||||||
package io.element.android.libraries.dateformatter.impl
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
class LocalDateTimeProvider @Inject constructor(
|
class LocalDateTimeProvider @Inject constructor(
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import io.element.android.libraries.dateformatter.impl.TimezoneProvider
|
import io.element.android.libraries.dateformatter.impl.TimezoneProvider
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.time.Clock
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.utils.allBooleans
|
import io.element.android.libraries.designsystem.utils.allBooleans
|
||||||
import kotlinx.datetime.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ import io.element.android.libraries.dateformatter.impl.DateFormatters
|
||||||
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
|
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
|
||||||
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatterDay
|
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatterDay
|
||||||
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
|
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create DefaultDateFormatter and set current time to the provided date.
|
* Create DefaultDateFormatter and set current time to the provided date.
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
|
|
||||||
package io.element.android.libraries.dateformatter.impl.previews
|
package io.element.android.libraries.dateformatter.impl.previews
|
||||||
|
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
class PreviewClock : Clock {
|
class PreviewClock : Clock {
|
||||||
private var instant: Instant = Instant.fromEpochMilliseconds(0)
|
private var instant: Instant = Instant.fromEpochMilliseconds(0)
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@ import android.os.Build
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@Config(qualifiers = "fr", sdk = [Build.VERSION_CODES.TIRAMISU])
|
@Config(qualifiers = "fr", sdk = [Build.VERSION_CODES.TIRAMISU])
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@ import android.os.Build
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@Config(qualifiers = "en", sdk = [Build.VERSION_CODES.TIRAMISU])
|
@Config(qualifiers = "en", sdk = [Build.VERSION_CODES.TIRAMISU])
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@
|
||||||
package io.element.android.libraries.dateformatter.impl
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
import io.element.android.tests.testutils.InstrumentationStringProvider
|
import io.element.android.tests.testutils.InstrumentationStringProvider
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create DefaultDateFormatter and set current time to the provided date.
|
* Create DefaultDateFormatter and set current time to the provided date.
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
|
|
||||||
package io.element.android.libraries.dateformatter.impl
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
class FakeClock : Clock {
|
class FakeClock : Clock {
|
||||||
private var instant: Instant = Instant.fromEpochMilliseconds(0)
|
private var instant: Instant = Instant.fromEpochMilliseconds(0)
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,10 @@ plugins {
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.maplibre.compose"
|
namespace = "io.element.android.libraries.maplibre.compose"
|
||||||
|
|
||||||
kotlinOptions {
|
kotlin {
|
||||||
freeCompilerArgs += "-Xexplicit-api=strict"
|
compilerOptions {
|
||||||
|
explicitApi()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue