Merge branch 'develop' into feature/fga/media_viewer_actions
This commit is contained in:
commit
76e35582da
155 changed files with 1143 additions and 1314 deletions
30
.github/workflows/nightlyReports.yml
vendored
30
.github/workflows/nightlyReports.yml
vendored
|
|
@ -17,7 +17,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'vector-im/element-x-android' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: ⏬ Checkout with LFS
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: 'true'
|
||||
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
|
|
@ -41,3 +45,27 @@ jobs:
|
|||
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
|
||||
if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
|
||||
run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
# Gradle dependency analysis using https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin
|
||||
dependency-analysis:
|
||||
name: Dependency analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.4.2
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Dependency analysis
|
||||
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload dependency analysis
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dependency-analysis
|
||||
path: build/reports/dependency-check-report.html
|
||||
|
|
|
|||
32
.github/workflows/quality.yml
vendored
32
.github/workflows/quality.yml
vendored
|
|
@ -66,35 +66,3 @@ jobs:
|
|||
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
|
||||
# Fallback for forks
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Gradle dependency analysis using https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin
|
||||
dependency-analysis:
|
||||
name: Dependency analysis
|
||||
runs-on: ubuntu-latest
|
||||
# Allow all jobs on main and develop. Just one per PR.
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('dep-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('dep-develop-{0}', github.sha) || format('dep-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/gradle-build-action@v2.4.2
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Dependency analysis
|
||||
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload dependency analysis
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dependency-analysis
|
||||
path: build/reports/dependency-check-report.html
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ appId: ${APP_ID}
|
|||
- inputText: ${ROOM_NAME.substring(0, 3)}
|
||||
- takeScreenshot: build/maestro/400-SearchRoom
|
||||
- tapOn: ${ROOM_NAME}
|
||||
# Close keyboard
|
||||
- hideKeyboard
|
||||
# Back from timeline
|
||||
- back
|
||||
# Close keyboard
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ appId: ${APP_ID}
|
|||
# TODO Create a room on a new account
|
||||
- tapOn: ${ROOM_NAME}
|
||||
- takeScreenshot: build/maestro/500-Timeline
|
||||
- tapOn: "Message…"
|
||||
- tapOn: "Message"
|
||||
- inputText: "Hello world!"
|
||||
- tapOn: "Toggle full screen mode"
|
||||
- tapOn: "Toggle full screen mode"
|
||||
- tapOn: "Send"
|
||||
- hideKeyboard
|
||||
- back
|
||||
|
|
|
|||
|
|
@ -216,7 +216,8 @@ dependencies {
|
|||
implementation(libs.coil)
|
||||
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||
implementation(libs.network.okhttp.logging)
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
implementation(libs.dagger)
|
||||
kapt(libs.dagger.compiler)
|
||||
|
|
|
|||
|
|
@ -158,10 +158,13 @@ koverMerged {
|
|||
"anvil.hint.merge.*",
|
||||
"anvil.module.*",
|
||||
"com.airbnb.android.showkase*",
|
||||
"io.element.android.libraries.designsystem.showkase.*",
|
||||
"*_Factory",
|
||||
"*_Factory$*",
|
||||
"*_Module",
|
||||
"*_Module$*",
|
||||
"*Module_Provides*",
|
||||
"Dagger*Component*",
|
||||
"*ComposableSingletons$*",
|
||||
"*_AssistedFactory_Impl*",
|
||||
"*BuildConfig",
|
||||
|
|
@ -175,6 +178,25 @@ koverMerged {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
annotations {
|
||||
excludes.addAll(
|
||||
listOf(
|
||||
"*Preview",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
projects {
|
||||
excludes.addAll(
|
||||
listOf(
|
||||
":anvilannotations",
|
||||
":anvilcodegen",
|
||||
":samples:minimal",
|
||||
":tests:testutils",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Run ./gradlew koverMergedVerify to check the rules.
|
||||
|
|
|
|||
1
changelog.d/484.feature
Normal file
1
changelog.d/484.feature
Normal file
|
|
@ -0,0 +1 @@
|
|||
New UI for composer and editing messages
|
||||
|
|
@ -94,7 +94,7 @@ internal fun DefaultInviteSummaryRow(
|
|||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.padding(16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -14,19 +14,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.features.messages.impl.attachments.preview
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -39,6 +34,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
|||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||
import io.element.android.features.messages.impl.media.local.LocalMediaView
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
|
|
@ -151,9 +147,8 @@ private fun AttachmentsPreviewBottomActions(
|
|||
onSendClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
ButtonRowMolecule(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
TextButton(onClick = onCancelClicked) {
|
||||
Text(stringResource(id = StringsR.string.action_cancel))
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
||||
import io.element.android.libraries.textcomposer.TextComposer
|
||||
|
||||
@Composable
|
||||
|
|
@ -52,17 +51,14 @@ fun MessageComposerView(
|
|||
|
||||
TextComposer(
|
||||
onSendMessage = ::sendMessage,
|
||||
fullscreen = state.isFullScreen,
|
||||
onFullscreenToggle = ::onFullscreenToggle,
|
||||
composerMode = state.mode,
|
||||
onCloseSpecialMode = ::onCloseSpecialMode,
|
||||
onResetComposerMode = ::onCloseSpecialMode,
|
||||
onComposerTextChange = ::onComposerTextChange,
|
||||
onAddAttachment = {
|
||||
state.eventSink(MessageComposerEvents.AddAttachment)
|
||||
},
|
||||
composerCanSendMessage = state.isSendButtonVisible,
|
||||
composerText = state.text?.charSequence?.toString(),
|
||||
isInDarkMode = !ElementTheme.colors.isLight,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package io.element.android.features.onboarding.impl
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -40,6 +39,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
|
@ -117,12 +117,7 @@ private fun OnBoardingButtons(
|
|||
onCreateAccount: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
ButtonColumnMolecule(modifier = modifier) {
|
||||
if (state.canLoginWithQrCode) {
|
||||
Button(
|
||||
onClick = {
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ internal fun TopicSection(
|
|||
Text(
|
||||
roomTopic.topic,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ private fun LabelledReadOnlyField(
|
|||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
text = title,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.roomlist.impl
|
|||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
|
|
@ -218,32 +217,34 @@ fun RoomListContent(
|
|||
|
||||
if (state.invitesState != InvitesState.NoInvites) {
|
||||
item {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
||||
.heightIn(min = 48.dp),
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(StringR.string.action_invites_list),
|
||||
fontSize = 14.sp,
|
||||
style = noFontPadding,
|
||||
)
|
||||
|
||||
if (state.invitesState == InvitesState.NewInvites) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.roomListUnreadIndicator())
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
||||
.padding(horizontal = 16.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.heightIn(min = 48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(StringR.string.action_invites_list),
|
||||
fontSize = 14.sp,
|
||||
style = noFontPadding,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(16.dp))
|
||||
if (state.invitesState == InvitesState.NewInvites) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.roomListUnreadIndicator())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,19 +17,15 @@
|
|||
package io.element.android.features.verifysession.impl
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -38,25 +34,21 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep
|
||||
|
|
@ -82,21 +74,18 @@ fun VerifySelfSessionView(
|
|||
val buttonsVisible by remember(verificationFlowStep) {
|
||||
derivedStateOf { verificationFlowStep != FlowStep.AwaitingOtherDeviceResponse && verificationFlowStep != FlowStep.Completed }
|
||||
}
|
||||
Surface {
|
||||
Column(modifier = modifier.systemBarsPadding()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
HeaderContent(verificationFlowStep = verificationFlowStep)
|
||||
Content(modifier = Modifier.weight(1f), flowState = verificationFlowStep)
|
||||
}
|
||||
HeaderFooterPage(
|
||||
modifier = modifier,
|
||||
header = {
|
||||
HeaderContent(verificationFlowStep = verificationFlowStep)
|
||||
},
|
||||
footer = {
|
||||
if (buttonsVisible) {
|
||||
BottomMenu(screenState = state, goBack = ::goBackAndCancelIfNeeded)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Content(flowState = verificationFlowStep)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,58 +110,22 @@ internal fun HeaderContent(verificationFlowStep: FlowStep, modifier: Modifier =
|
|||
FlowStep.Ready, is FlowStep.Verifying, FlowStep.Completed -> R.string.screen_session_verification_compare_emojis_subtitle
|
||||
}
|
||||
|
||||
Column(modifier) {
|
||||
Spacer(Modifier.height(80.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(width = 70.dp, height = 70.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.background(
|
||||
color = LocalColors.current.quinary,
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(68.dp))
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(width = 48.dp, height = 48.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
resourceId = iconResourceId,
|
||||
contentDescription = "",
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = titleTextId),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterHorizontally),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTextStyles.Bold.title2,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(id = subtitleTextId),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTextStyles.Regular.subheadline,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = modifier.padding(top = 60.dp),
|
||||
iconResourceId = iconResourceId,
|
||||
title = stringResource(id = titleTextId),
|
||||
subTitle = stringResource(id = subtitleTextId)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun Content(flowState: FlowStep, modifier: Modifier = Modifier) {
|
||||
Column(modifier, verticalArrangement = Arrangement.Center) {
|
||||
Spacer(Modifier.shrinkableHeight(min = 20.dp, max = 56.dp))
|
||||
Column(modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
|
||||
when (flowState) {
|
||||
FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit
|
||||
FlowStep.AwaitingOtherDeviceResponse -> ContentWaiting()
|
||||
is FlowStep.Verifying -> ContentVerifying(flowState)
|
||||
}
|
||||
Spacer(Modifier.shrinkableHeight(min = 20.dp, max = 56.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,11 +212,8 @@ internal fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
|
|||
else -> goBack
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
ButtonColumnMolecule(
|
||||
modifier = Modifier.padding(bottom = 20.dp)
|
||||
) {
|
||||
ButtonWithProgress(
|
||||
text = positiveButtonTitle?.let { stringResource(it) },
|
||||
|
|
@ -272,7 +222,6 @@ internal fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
|
|||
onClick = { positiveButtonEvent?.let { eventSink(it) } }
|
||||
)
|
||||
if (negativeButtonTitle != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = negativeButtonCallback,
|
||||
|
|
@ -281,7 +230,6 @@ internal fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
|
|||
Text(stringResource(negativeButtonTitle), fontSize = 16.sp)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(40.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -302,15 +250,3 @@ private fun ContentToPreview(state: VerifySelfSessionState) {
|
|||
goBack = {},
|
||||
)
|
||||
}
|
||||
|
||||
private fun Modifier.shrinkableHeight(
|
||||
min: Dp,
|
||||
max: Dp,
|
||||
minScreenHeight: Int = 720
|
||||
): Modifier = composed {
|
||||
if (LocalConfiguration.current.screenHeightDp >= minScreenHeight) {
|
||||
then(Modifier.height(max))
|
||||
} else {
|
||||
then(Modifier.height(min))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ squareup_seismic = "com.squareup:seismic:1.0.3"
|
|||
|
||||
# network
|
||||
network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.11.0"
|
||||
network_okhttp_logging = { module = "com.squareup.okhttp3:logging-interceptor" }
|
||||
network_okhttp = { module = "com.squareup.okhttp3:okhttp" }
|
||||
network_retrofit = "com.squareup.retrofit2:retrofit:2.9.0"
|
||||
network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
|
||||
|
||||
|
|
@ -181,7 +183,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
|
|||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
anvil = { id = "com.squareup.anvil", version.ref = "anvil" }
|
||||
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
|
||||
ktlint = "org.jlleitschuh.gradle.ktlint:11.3.2"
|
||||
ktlint = "org.jlleitschuh.gradle.ktlint:11.4.0"
|
||||
dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" }
|
||||
dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" }
|
||||
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.molecules
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
|
||||
@Composable
|
||||
fun ButtonColumnMolecule(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun ButtonColumnMoleculeLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun ButtonColumnMoleculeDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
ButtonColumnMolecule {
|
||||
Button(onClick = {}, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = "Button")
|
||||
}
|
||||
TextButton(onClick = {}, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = "TextButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.molecules
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
|
||||
@Composable
|
||||
fun ButtonRowMolecule(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun ButtonRowMoleculeLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun ButtonRowMoleculeDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
ButtonRowMolecule {
|
||||
TextButton(onClick = { }) {
|
||||
Text("Button 1")
|
||||
}
|
||||
TextButton(onClick = { }) {
|
||||
Text("Button 2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.molecules
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.R
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
fun IconTitleSubtitleMolecule(
|
||||
iconResourceId: Int,
|
||||
title: String,
|
||||
subTitle: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(width = 70.dp, height = 70.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.background(
|
||||
color = LocalColors.current.quinary,
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(width = 48.dp, height = 48.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
resourceId = iconResourceId,
|
||||
contentDescription = "",
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTextStyles.Bold.title2,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = subTitle,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = ElementTextStyles.Regular.subheadline,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun IconTitleSubtitleMoleculeLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun IconTitleSubtitleMoleculeDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
IconTitleSubtitleMolecule(
|
||||
iconResourceId = R.drawable.ic_edit,
|
||||
title = "Title",
|
||||
subTitle = "Sub iitle",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.pages
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
/**
|
||||
* @param modifier Classical modifier.
|
||||
* @param header optional header.
|
||||
* @param footer optional footer.
|
||||
* @param content main content.
|
||||
*/
|
||||
@Composable
|
||||
fun HeaderFooterPage(
|
||||
modifier: Modifier = Modifier,
|
||||
header: @Composable () -> Unit = {},
|
||||
footer: @Composable () -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.padding(all = 20.dp),
|
||||
) {
|
||||
// Header
|
||||
header()
|
||||
// Content
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
// Footer
|
||||
footer()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun HeaderFooterPageLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun HeaderFooterPageDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
HeaderFooterPage(
|
||||
content = {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Content",
|
||||
fontSize = 40.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
header = {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Header",
|
||||
fontSize = 40.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
footer = {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Footer",
|
||||
fontSize = 40.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
|
|
@ -47,6 +48,7 @@ fun LabelledTextField(
|
|||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
text = label
|
||||
)
|
||||
|
|
|
|||
|
|
@ -72,13 +72,13 @@ private fun ProgressDialogContent(
|
|||
modifier = Modifier.padding(top = 38.dp, bottom = 32.dp, start = 40.dp, end = 40.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
if (!text.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
|
|
@ -48,7 +49,7 @@ fun PreferenceCategory(
|
|||
}
|
||||
content()
|
||||
if (showDivider) {
|
||||
PreferenceDivider()
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.preferences
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
|
||||
@Composable
|
||||
fun PreferenceDivider(modifier: Modifier = Modifier) {
|
||||
Divider(modifier, thickness = 0.5.dp)
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Dividers)
|
||||
@Composable
|
||||
internal fun PreferenceDividerPreview() {
|
||||
ElementThemedPreview {
|
||||
Box(Modifier.padding(vertical = 10.dp), contentAlignment = Alignment.Center) {
|
||||
PreferenceDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ import androidx.compose.ui.unit.sp
|
|||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
|
|
@ -131,13 +132,13 @@ private fun ContentToPreview() {
|
|||
subtitle = "Some other text",
|
||||
icon = Icons.Default.BugReport,
|
||||
)
|
||||
PreferenceDivider()
|
||||
Divider()
|
||||
PreferenceSwitch(
|
||||
title = "Switch",
|
||||
icon = Icons.Default.Announcement,
|
||||
isChecked = true,
|
||||
)
|
||||
PreferenceDivider()
|
||||
Divider()
|
||||
PreferenceSlide(
|
||||
title = "Slide",
|
||||
summary = "Summary",
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ fun PreferenceText(
|
|||
}
|
||||
if (subtitle != null) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = subtitle,
|
||||
color = tintColor ?: MaterialTheme.colorScheme.tertiary,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ fun elementColorsDark() = ElementColors(
|
|||
gray400 = Compound_Gray_400_Dark,
|
||||
gray1400 = Compound_Gray_1400_Dark,
|
||||
textActionCritical = TextColorCriticalDark,
|
||||
accentColor = Color(0xFF0DBD8B),
|
||||
placeholder = Compound_Gray_800_Dark,
|
||||
isLight = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ fun elementColorsLight() = ElementColors(
|
|||
gray400 = Compound_Gray_400_Light,
|
||||
gray1400 = Compound_Gray_1400_Light,
|
||||
textActionCritical = TextColorCriticalLight,
|
||||
accentColor = Color(0xFF0DBD8B),
|
||||
placeholder = Compound_Gray_800_Light,
|
||||
isLight = true,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class ElementColors(
|
|||
gray400: Color,
|
||||
gray1400: Color,
|
||||
textActionCritical: Color,
|
||||
accentColor: Color,
|
||||
placeholder: Color,
|
||||
isLight: Boolean
|
||||
) {
|
||||
|
|
@ -61,6 +62,9 @@ class ElementColors(
|
|||
var textActionCritical by mutableStateOf(textActionCritical)
|
||||
private set
|
||||
|
||||
var accentColor by mutableStateOf(accentColor)
|
||||
private set
|
||||
|
||||
var placeholder by mutableStateOf(placeholder)
|
||||
private set
|
||||
|
||||
|
|
@ -77,6 +81,7 @@ class ElementColors(
|
|||
gray400: Color = this.gray400,
|
||||
gray1400: Color = this.gray1400,
|
||||
textActionCritical: Color = this.textActionCritical,
|
||||
accentColor: Color = this.accentColor,
|
||||
placeholder: Color = this.placeholder,
|
||||
isLight: Boolean = this.isLight,
|
||||
) = ElementColors(
|
||||
|
|
@ -89,6 +94,7 @@ class ElementColors(
|
|||
gray400 = gray400,
|
||||
gray1400 = gray1400,
|
||||
textActionCritical = textActionCritical,
|
||||
accentColor = accentColor,
|
||||
placeholder = placeholder,
|
||||
isLight = isLight,
|
||||
)
|
||||
|
|
@ -103,6 +109,7 @@ class ElementColors(
|
|||
gray400 = other.gray400
|
||||
gray1400 = other.gray1400
|
||||
textActionCritical = other.textActionCritical
|
||||
accentColor = other.accentColor
|
||||
placeholder = other.placeholder
|
||||
isLight = other.isLight
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
|||
@Composable
|
||||
fun Divider(
|
||||
modifier: Modifier = Modifier,
|
||||
thickness: Dp = DividerDefaults.Thickness,
|
||||
thickness: Dp = ElementDividerDefaults.thickness,
|
||||
color: Color = DividerDefaults.color,
|
||||
) {
|
||||
androidx.compose.material3.Divider(
|
||||
|
|
@ -42,6 +42,10 @@ fun Divider(
|
|||
)
|
||||
}
|
||||
|
||||
object ElementDividerDefaults {
|
||||
val thickness = 0.5.dp
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Dividers)
|
||||
@Composable
|
||||
internal fun DividerPreview() = ElementThemedPreview {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import androidx.compose.ui.input.key.key
|
|||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -95,6 +96,53 @@ fun OutlinedTextField(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OutlinedTextField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
supportingText: @Composable (() -> Unit)? = null,
|
||||
isError: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = OutlinedTextFieldDefaults.shape,
|
||||
colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
|
||||
) {
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
supportingText = supportingText,
|
||||
isError = isError,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
fun Modifier.onTabOrEnterKeyFocusNext(focusManager: FocusManager): Modifier = onPreviewKeyEvent { event ->
|
||||
if (event.key == Key.Tab || event.key == Key.Enter) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||
import androidx.compose.ui.platform.LocalAutofill
|
||||
import androidx.compose.ui.platform.LocalAutofillTree
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -96,6 +97,53 @@ fun TextField(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
supportingText: @Composable (() -> Unit)? = null,
|
||||
isError: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors()
|
||||
) {
|
||||
androidx.compose.material3.TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
supportingText = supportingText,
|
||||
isError = isError,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.TextFields)
|
||||
@Composable
|
||||
internal fun TextFieldLightPreview() =
|
||||
|
|
|
|||
|
|
@ -31,9 +31,8 @@ dependencies {
|
|||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation("com.squareup.okhttp3:okhttp")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||
|
||||
implementation(libs.network.okhttp)
|
||||
implementation(libs.network.okhttp.logging)
|
||||
implementation(libs.network.retrofit)
|
||||
implementation(libs.network.retrofit.converter.serialization)
|
||||
implementation(libs.serialization.json)
|
||||
|
|
|
|||
|
|
@ -23,41 +23,36 @@ import io.element.android.libraries.core.meta.BuildMeta
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@JvmStatic
|
||||
fun providesHttpLoggingInterceptor(buildMeta: BuildMeta): HttpLoggingInterceptor {
|
||||
val loggingLevel = if (buildMeta.isDebuggable) {
|
||||
HttpLoggingInterceptor.Level.BODY
|
||||
} else {
|
||||
HttpLoggingInterceptor.Level.BASIC
|
||||
}
|
||||
val logger = FormattedJsonHttpLogger(loggingLevel)
|
||||
val interceptor = HttpLoggingInterceptor(logger)
|
||||
interceptor.level = loggingLevel
|
||||
return interceptor
|
||||
}
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesOkHttpClient(
|
||||
httpLoggingInterceptor: HttpLoggingInterceptor,
|
||||
): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
// workaround for #4669
|
||||
.protocols(listOf(Protocol.HTTP_1_1))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.addInterceptor(httpLoggingInterceptor)
|
||||
.build()
|
||||
buildMeta: BuildMeta,
|
||||
): OkHttpClient = OkHttpClient.Builder().apply {
|
||||
connectTimeout(30, TimeUnit.SECONDS)
|
||||
readTimeout(60, TimeUnit.SECONDS)
|
||||
writeTimeout(60, TimeUnit.SECONDS)
|
||||
if (buildMeta.isDebuggable) addInterceptor(providesHttpLoggingInterceptor())
|
||||
}.build()
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesJson(): Json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor {
|
||||
val loggingLevel = HttpLoggingInterceptor.Level.BODY
|
||||
val logger = FormattedJsonHttpLogger(loggingLevel)
|
||||
val interceptor = HttpLoggingInterceptor(logger)
|
||||
interceptor.level = loggingLevel
|
||||
return interceptor
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,23 +17,21 @@
|
|||
package io.element.android.libraries.network
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import dagger.Lazy
|
||||
import io.element.android.libraries.core.uri.ensureTrailingSlash
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
class RetrofitFactory @Inject constructor(
|
||||
private val okHttpClient: Lazy<OkHttpClient>,
|
||||
private val okHttpClient: Provider<OkHttpClient>,
|
||||
private val json: Provider<Json>,
|
||||
) {
|
||||
fun create(baseUrl: String): Retrofit {
|
||||
val contentType = "application/json".toMediaType()
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl.ensureTrailingSlash())
|
||||
.addConverterFactory(Json.asConverterFactory(contentType))
|
||||
.callFactory { request -> okHttpClient.get().newCall(request) }
|
||||
.build()
|
||||
}
|
||||
fun create(baseUrl: String): Retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl.ensureTrailingSlash())
|
||||
.addConverterFactory(json.get().asConverterFactory("application/json".toMediaType()))
|
||||
.callFactory(okHttpClient.get())
|
||||
.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.textcomposer
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.Editable
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
|
||||
// Imported from Element Android
|
||||
interface MessageComposerView {
|
||||
|
||||
companion object {
|
||||
const val MAX_LINES_WHEN_COLLAPSED = 10
|
||||
}
|
||||
|
||||
val text: Editable?
|
||||
val formattedText: String?
|
||||
val editText: EditText
|
||||
val emojiButton: ImageButton?
|
||||
val sendButton: ImageButton
|
||||
val attachmentButton: ImageButton
|
||||
|
||||
var callback: Callback?
|
||||
|
||||
fun setTextIfDifferent(text: CharSequence?): Boolean
|
||||
fun renderComposerMode(mode: MessageComposerMode)
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
// From ComposerEditText.Callback
|
||||
fun onRichContentSelected(contentUri: Uri): Boolean
|
||||
|
||||
// From ComposerEditText.Callback
|
||||
fun onTextChanged(text: CharSequence)
|
||||
|
||||
fun onCloseRelatedMessage()
|
||||
fun onSendMessage(text: CharSequence)
|
||||
fun onAddAttachment()
|
||||
fun onExpandOrCompactChange()
|
||||
fun onFullScreenModeChanged()
|
||||
}
|
||||
|
|
@ -1,552 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.textcomposer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import io.element.android.libraries.androidutils.ui.DimensionConverter
|
||||
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||
import io.element.android.libraries.androidutils.ui.showKeyboard
|
||||
import io.element.android.libraries.textcomposer.databinding.ComposerRichTextLayoutBinding
|
||||
import io.element.android.libraries.textcomposer.databinding.ViewRichTextMenuButtonBinding
|
||||
import io.element.android.libraries.textcomposer.tools.setTextIfDifferent
|
||||
import io.element.android.wysiwyg.EditorEditText
|
||||
import io.element.android.wysiwyg.view.models.InlineFormat
|
||||
import uniffi.wysiwyg_composer.ActionState
|
||||
import uniffi.wysiwyg_composer.ComposerAction
|
||||
import io.element.android.libraries.resources.R as ElementR
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
// Imported from Element Android
|
||||
class RichTextComposerLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyleAttr), MessageComposerView {
|
||||
|
||||
private val views: ComposerRichTextLayoutBinding
|
||||
|
||||
override var callback: Callback? = null
|
||||
|
||||
// There is no need to persist these values since they're always updated by the parent fragment
|
||||
private var isFullScreen = false
|
||||
private var hasRelatedMessage = false
|
||||
private var composerMode: MessageComposerMode? = null
|
||||
|
||||
var isTextFormattingEnabled = true
|
||||
set(value) {
|
||||
if (field == value) return
|
||||
syncEditTexts()
|
||||
field = value
|
||||
updateTextFieldBorder(isFullScreen)
|
||||
updateEditTextVisibility()
|
||||
updateFullScreenButtonVisibility()
|
||||
// If formatting is no longer enabled and it's in full screen, minimise the editor
|
||||
if (!value && isFullScreen) {
|
||||
callback?.onFullScreenModeChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override val text: Editable?
|
||||
get() = editText.text
|
||||
override val formattedText: String?
|
||||
get() = (editText as? EditorEditText)?.getHtmlOutput()
|
||||
override val editText: EditText
|
||||
get() = if (isTextFormattingEnabled) {
|
||||
views.richTextComposerEditText
|
||||
} else {
|
||||
views.plainTextComposerEditText
|
||||
}
|
||||
override val emojiButton: ImageButton?
|
||||
get() = null
|
||||
override val sendButton: ImageButton
|
||||
get() = views.sendButton
|
||||
override val attachmentButton: ImageButton
|
||||
get() = views.attachmentButton
|
||||
|
||||
// Border of the EditText
|
||||
private val borderShapeDrawable: MaterialShapeDrawable by lazy {
|
||||
MaterialShapeDrawable().apply {
|
||||
val typedData = TypedValue()
|
||||
val lineColor = context.theme.obtainStyledAttributes(
|
||||
typedData.data,
|
||||
intArrayOf(ElementR.attr.vctr_content_quaternary)
|
||||
)
|
||||
.getColor(0, 0)
|
||||
strokeColor = ColorStateList.valueOf(lineColor)
|
||||
strokeWidth = 1 * resources.displayMetrics.scaledDensity
|
||||
fillColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
val cornerSize =
|
||||
resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line)
|
||||
setCornerSize(cornerSize.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
private val dimensionConverter = DimensionConverter(resources)
|
||||
|
||||
fun setFullScreen(isFullScreen: Boolean, animated: Boolean, manageKeyboard: Boolean) {
|
||||
if (!animated && views.composerLayout.layoutParams != null) {
|
||||
views.composerLayout.updateLayoutParams<ViewGroup.LayoutParams> {
|
||||
height =
|
||||
if (isFullScreen) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
editText.updateLayoutParams<ViewGroup.LayoutParams> {
|
||||
height = if (isFullScreen) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
|
||||
updateTextFieldBorder(isFullScreen)
|
||||
updateEditTextVisibility()
|
||||
|
||||
updateEditTextFullScreenState(views.richTextComposerEditText, isFullScreen)
|
||||
updateEditTextFullScreenState(views.plainTextComposerEditText, isFullScreen)
|
||||
|
||||
views.composerFullScreenButton.setImageResource(
|
||||
if (isFullScreen) R.drawable.ic_composer_collapse else R.drawable.ic_composer_full_screen
|
||||
)
|
||||
|
||||
views.bottomSheetHandle.isVisible = false // EAx: always gone, we do not have a bottom sheet here. // isFullScreen
|
||||
if (manageKeyboard) {
|
||||
if (isFullScreen) {
|
||||
editText.showKeyboard(true)
|
||||
} else {
|
||||
editText.hideKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
this.isFullScreen = isFullScreen
|
||||
}
|
||||
|
||||
fun notifyIsBeingDragged(percentage: Float) {
|
||||
// Calculate a new shape for the border according to the position in screen
|
||||
val isSingleLine = editText.lineCount == 1
|
||||
val cornerSize = if (!isSingleLine || hasRelatedMessage) {
|
||||
resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded)
|
||||
.toFloat()
|
||||
} else {
|
||||
val multilineCornerSize =
|
||||
resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded)
|
||||
val singleLineCornerSize =
|
||||
resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line)
|
||||
val diff = singleLineCornerSize - multilineCornerSize
|
||||
multilineCornerSize + diff * (1 - percentage)
|
||||
}
|
||||
if (cornerSize != borderShapeDrawable.bottomLeftCornerResolvedSize) {
|
||||
borderShapeDrawable.setCornerSize(cornerSize)
|
||||
}
|
||||
|
||||
// Change maxLines while dragging, this should improve the smoothness of animations
|
||||
val maxLines = if (percentage > 0.25f) {
|
||||
Int.MAX_VALUE
|
||||
} else {
|
||||
MessageComposerView.MAX_LINES_WHEN_COLLAPSED
|
||||
}
|
||||
views.richTextComposerEditText.maxLines = maxLines
|
||||
views.plainTextComposerEditText.maxLines = maxLines
|
||||
|
||||
views.bottomSheetHandle.isVisible = false // EAx: always gone, we do not have a bottom sheet here.
|
||||
}
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.composer_rich_text_layout, this)
|
||||
views = ComposerRichTextLayoutBinding.bind(this)
|
||||
|
||||
// Workaround to avoid cut-off text caused by padding in scrolled TextView (there is no clipToPadding).
|
||||
// In TextView, clipTop = padding, but also clipTop -= shadowRadius. So if we set the shadowRadius to padding, they cancel each other
|
||||
views.richTextComposerEditText.setShadowLayer(
|
||||
views.richTextComposerEditText.paddingBottom.toFloat(),
|
||||
0f,
|
||||
0f,
|
||||
0
|
||||
)
|
||||
views.plainTextComposerEditText.setShadowLayer(
|
||||
views.richTextComposerEditText.paddingBottom.toFloat(),
|
||||
0f,
|
||||
0f,
|
||||
0
|
||||
)
|
||||
|
||||
renderComposerMode(MessageComposerMode.Normal(null))
|
||||
|
||||
views.richTextComposerEditText.addTextChangedListener(
|
||||
TextChangeListener(
|
||||
{ callback?.onTextChanged(it) },
|
||||
{ updateTextFieldBorder(isFullScreen) })
|
||||
)
|
||||
views.plainTextComposerEditText.addTextChangedListener(
|
||||
TextChangeListener(
|
||||
{ callback?.onTextChanged(it) },
|
||||
{ updateTextFieldBorder(isFullScreen) })
|
||||
)
|
||||
|
||||
disallowParentInterceptTouchEvent(views.richTextComposerEditText)
|
||||
disallowParentInterceptTouchEvent(views.plainTextComposerEditText)
|
||||
|
||||
views.composerModeCloseView.setOnClickListener {
|
||||
callback?.onCloseRelatedMessage()
|
||||
}
|
||||
|
||||
views.sendButton.setOnClickListener {
|
||||
val textMessage =
|
||||
views.richTextComposerEditText.getMarkdown() // text?.toSpannable() ?: ""
|
||||
callback?.onSendMessage(textMessage)
|
||||
}
|
||||
|
||||
views.attachmentButton.setOnClickListener {
|
||||
callback?.onAddAttachment()
|
||||
}
|
||||
|
||||
views.composerFullScreenButton.apply {
|
||||
updateFullScreenButtonVisibility()
|
||||
setOnClickListener {
|
||||
callback?.onFullScreenModeChanged()
|
||||
}
|
||||
}
|
||||
|
||||
views.composerEditTextOuterBorder.background = borderShapeDrawable
|
||||
|
||||
setupRichTextMenu()
|
||||
|
||||
updateTextFieldBorder(isFullScreen)
|
||||
}
|
||||
|
||||
private fun setupRichTextMenu() {
|
||||
addRichTextMenuItem(
|
||||
R.drawable.ic_composer_bold,
|
||||
R.string.rich_text_editor_format_bold,
|
||||
ComposerAction.BOLD
|
||||
) {
|
||||
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold)
|
||||
}
|
||||
addRichTextMenuItem(
|
||||
R.drawable.ic_composer_italic,
|
||||
R.string.rich_text_editor_format_italic,
|
||||
ComposerAction.ITALIC
|
||||
) {
|
||||
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic)
|
||||
}
|
||||
addRichTextMenuItem(
|
||||
R.drawable.ic_composer_underlined,
|
||||
R.string.rich_text_editor_format_underline,
|
||||
ComposerAction.UNDERLINE
|
||||
) {
|
||||
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline)
|
||||
}
|
||||
addRichTextMenuItem(
|
||||
R.drawable.ic_composer_strikethrough,
|
||||
R.string.rich_text_editor_format_strikethrough,
|
||||
ComposerAction.STRIKE_THROUGH
|
||||
) {
|
||||
views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun disallowParentInterceptTouchEvent(view: View) {
|
||||
view.setOnTouchListener { v, event ->
|
||||
if (v.hasFocus()) {
|
||||
v.parent?.requestDisallowInterceptTouchEvent(true)
|
||||
val action = event.actionMasked
|
||||
if (action == MotionEvent.ACTION_SCROLL) {
|
||||
v.parent?.requestDisallowInterceptTouchEvent(false)
|
||||
return@setOnTouchListener true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
views.richTextComposerEditText.actionStatesChangedListener =
|
||||
EditorEditText.OnActionStatesChangedListener { state ->
|
||||
for (action in state.keys) {
|
||||
updateMenuStateFor(action, state)
|
||||
}
|
||||
}
|
||||
|
||||
updateEditTextVisibility()
|
||||
}
|
||||
|
||||
private fun updateEditTextVisibility() {
|
||||
views.richTextComposerEditText.isVisible = isTextFormattingEnabled
|
||||
views.richTextMenu.isVisible = isTextFormattingEnabled
|
||||
views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled
|
||||
|
||||
// The layouts for formatted text mode and plain text mode are different, so we need to update the constraints
|
||||
val dpToPx = { dp: Int -> dimensionConverter.dpToPx(dp) }
|
||||
ConstraintSet().apply {
|
||||
clone(views.composerLayoutContent)
|
||||
clear(R.id.composerEditTextOuterBorder, ConstraintSet.TOP)
|
||||
clear(R.id.composerEditTextOuterBorder, ConstraintSet.BOTTOM)
|
||||
clear(R.id.composerEditTextOuterBorder, ConstraintSet.START)
|
||||
clear(R.id.composerEditTextOuterBorder, ConstraintSet.END)
|
||||
if (isTextFormattingEnabled) {
|
||||
connect(
|
||||
R.id.composerEditTextOuterBorder,
|
||||
ConstraintSet.TOP,
|
||||
R.id.composerLayoutContent,
|
||||
ConstraintSet.TOP,
|
||||
dpToPx(8)
|
||||
)
|
||||
connect(
|
||||
R.id.composerEditTextOuterBorder,
|
||||
ConstraintSet.BOTTOM,
|
||||
R.id.sendButton,
|
||||
ConstraintSet.TOP,
|
||||
0
|
||||
)
|
||||
connect(
|
||||
R.id.composerEditTextOuterBorder,
|
||||
ConstraintSet.START,
|
||||
R.id.composerLayoutContent,
|
||||
ConstraintSet.START,
|
||||
dpToPx(12)
|
||||
)
|
||||
connect(
|
||||
R.id.composerEditTextOuterBorder,
|
||||
ConstraintSet.END,
|
||||
R.id.composerLayoutContent,
|
||||
ConstraintSet.END,
|
||||
dpToPx(12)
|
||||
)
|
||||
} else {
|
||||
connect(
|
||||
R.id.composerEditTextOuterBorder,
|
||||
ConstraintSet.TOP,
|
||||
R.id.composerLayoutContent,
|
||||
ConstraintSet.TOP,
|
||||
dpToPx(10)
|
||||
)
|
||||
connect(
|
||||
R.id.composerEditTextOuterBorder,
|
||||
ConstraintSet.BOTTOM,
|
||||
R.id.composerLayoutContent,
|
||||
ConstraintSet.BOTTOM,
|
||||
dpToPx(10)
|
||||
)
|
||||
connect(
|
||||
R.id.composerEditTextOuterBorder,
|
||||
ConstraintSet.START,
|
||||
R.id.attachmentButton,
|
||||
ConstraintSet.END,
|
||||
0
|
||||
)
|
||||
connect(
|
||||
R.id.composerEditTextOuterBorder,
|
||||
ConstraintSet.END,
|
||||
R.id.sendButton,
|
||||
ConstraintSet.START,
|
||||
0
|
||||
)
|
||||
}
|
||||
applyTo(views.composerLayoutContent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFullScreenButtonVisibility() {
|
||||
val isLargeScreenDevice =
|
||||
resources.configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
// There's no point in having full screen in landscape since there's almost no vertical space
|
||||
views.composerFullScreenButton.isInvisible =
|
||||
!isTextFormattingEnabled || (isLandscape && !isLargeScreenDevice)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the non-active input with the contents of the active input.
|
||||
*/
|
||||
private fun syncEditTexts() =
|
||||
if (isTextFormattingEnabled) {
|
||||
views.plainTextComposerEditText.setText(views.richTextComposerEditText.getMarkdown())
|
||||
} else {
|
||||
views.richTextComposerEditText.setMarkdown(views.plainTextComposerEditText.text.toString())
|
||||
}
|
||||
|
||||
private fun addRichTextMenuItem(
|
||||
@DrawableRes iconId: Int,
|
||||
@StringRes description: Int,
|
||||
action: ComposerAction,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true)
|
||||
button.root.tag = action
|
||||
with(button.root) {
|
||||
contentDescription = resources.getString(description)
|
||||
setImageResource(iconId)
|
||||
setOnClickListener {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMenuStateFor(
|
||||
action: ComposerAction,
|
||||
menuState: Map<ComposerAction, ActionState>
|
||||
) {
|
||||
val button = findViewWithTag<ImageButton>(action) ?: return
|
||||
val stateForAction = menuState[action]
|
||||
button.isEnabled = stateForAction != ActionState.DISABLED
|
||||
button.isSelected = stateForAction == ActionState.REVERSED
|
||||
}
|
||||
|
||||
fun estimateCollapsedHeight(): Int {
|
||||
val editText = this.editText
|
||||
val originalLines = editText.maxLines
|
||||
val originalParamsHeight = editText.layoutParams.height
|
||||
editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED
|
||||
editText.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
measure(
|
||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.UNSPECIFIED,
|
||||
)
|
||||
val result = measuredHeight
|
||||
editText.layoutParams.height = originalParamsHeight
|
||||
editText.maxLines = originalLines
|
||||
return result
|
||||
}
|
||||
|
||||
private fun updateTextFieldBorder(isFullScreen: Boolean) {
|
||||
val isMultiline =
|
||||
editText.editableText.lines().count() > 1 || isFullScreen || hasRelatedMessage
|
||||
val cornerSize = if (isMultiline) {
|
||||
resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded)
|
||||
} else {
|
||||
resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line)
|
||||
}.toFloat()
|
||||
borderShapeDrawable.setCornerSize(cornerSize)
|
||||
}
|
||||
|
||||
private fun replaceFormattedContent(text: CharSequence) {
|
||||
views.richTextComposerEditText.setHtml(text.toString())
|
||||
updateTextFieldBorder(isFullScreen)
|
||||
}
|
||||
|
||||
override fun setTextIfDifferent(text: CharSequence?): Boolean {
|
||||
val result = editText.setTextIfDifferent(text)
|
||||
updateTextFieldBorder(isFullScreen)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun updateEditTextFullScreenState(editText: EditText, isFullScreen: Boolean) {
|
||||
if (isFullScreen) {
|
||||
editText.maxLines = Int.MAX_VALUE
|
||||
} else {
|
||||
editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED
|
||||
}
|
||||
}
|
||||
|
||||
override fun renderComposerMode(mode: MessageComposerMode) {
|
||||
if (this.composerMode == mode) return
|
||||
this.composerMode = mode
|
||||
|
||||
if (mode is MessageComposerMode.Special) {
|
||||
views.composerModeGroup.isVisible = true
|
||||
replaceFormattedContent(mode.defaultContent)
|
||||
hasRelatedMessage = true
|
||||
editText.showKeyboard(andRequestFocus = true)
|
||||
} else {
|
||||
views.composerModeGroup.isGone = true
|
||||
(mode as? MessageComposerMode.Normal)?.content?.let { text ->
|
||||
// TODO un-comment once we update to a version of the lib > 0.8.0
|
||||
/*
|
||||
if (isTextFormattingEnabled) {
|
||||
replaceFormattedContent(text)
|
||||
} else {
|
||||
views.plainTextComposerEditText.setText(text)
|
||||
}
|
||||
*/
|
||||
views.plainTextComposerEditText.setText(text)
|
||||
}
|
||||
views.sendButton.contentDescription = resources.getString(StringR.string.action_send)
|
||||
hasRelatedMessage = false
|
||||
}
|
||||
|
||||
views.sendButton.apply {
|
||||
if (mode is MessageComposerMode.Edit) {
|
||||
contentDescription = resources.getString(StringR.string.action_save)
|
||||
setImageResource(R.drawable.ic_composer_rich_text_save)
|
||||
} else {
|
||||
contentDescription = resources.getString(StringR.string.action_send)
|
||||
setImageResource(R.drawable.ic_rich_composer_send)
|
||||
}
|
||||
}
|
||||
|
||||
updateTextFieldBorder(isFullScreen)
|
||||
|
||||
when (mode) {
|
||||
is MessageComposerMode.Edit -> {
|
||||
views.composerModeTitleView.setText(R.string.editing)
|
||||
views.composerModeIconView.setImageResource(R.drawable.ic_composer_rich_text_editor_edit)
|
||||
}
|
||||
is MessageComposerMode.Quote -> {
|
||||
views.composerModeTitleView.setText(R.string.quoting)
|
||||
views.composerModeIconView.setImageResource(R.drawable.ic_quote)
|
||||
}
|
||||
is MessageComposerMode.Reply -> {
|
||||
val userName = mode.senderName
|
||||
views.composerModeTitleView.text =
|
||||
resources.getString(R.string.replying_to, userName)
|
||||
views.composerModeIconView.setImageResource(R.drawable.ic_reply)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private class TextChangeListener(
|
||||
private val onTextChanged: (s: Editable) -> Unit,
|
||||
private val onExpandedChanged: (isExpanded: Boolean) -> Unit,
|
||||
) : TextWatcher {
|
||||
private var previousTextWasExpanded = false
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
onTextChanged.invoke(s)
|
||||
|
||||
val isExpanded = s.lines().count() > 1
|
||||
if (previousTextWasExpanded != isExpanded) {
|
||||
onExpandedChanged(isExpanded)
|
||||
}
|
||||
previousTextWasExpanded = isExpanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,140 +16,275 @@
|
|||
|
||||
package io.element.android.libraries.textcomposer
|
||||
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.VectorIcons
|
||||
import io.element.android.libraries.designsystem.modifiers.applyIf
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TextComposer(
|
||||
fullscreen: Boolean,
|
||||
composerText: String?,
|
||||
composerMode: MessageComposerMode,
|
||||
composerCanSendMessage: Boolean,
|
||||
isInDarkMode: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
focusRequester: FocusRequester = FocusRequester(),
|
||||
onSendMessage: (String) -> Unit = {},
|
||||
onFullscreenToggle: () -> Unit = {},
|
||||
onCloseSpecialMode: () -> Unit = {},
|
||||
onResetComposerMode: () -> Unit = {},
|
||||
onComposerTextChange: (CharSequence) -> Unit = {},
|
||||
onAddAttachment:() -> Unit = {},
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
FakeComposer(modifier)
|
||||
} else {
|
||||
val focusRequester = FocusRequester()
|
||||
AndroidView(
|
||||
modifier = modifier.focusRequester(focusRequester),
|
||||
factory = { context ->
|
||||
RichTextComposerLayout(context).apply {
|
||||
// Sets up listeners for View -> Compose communication
|
||||
this.callback = object : Callback {
|
||||
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onTextChanged(text: CharSequence) {
|
||||
onComposerTextChange(text)
|
||||
}
|
||||
|
||||
override fun onCloseRelatedMessage() {
|
||||
onCloseSpecialMode()
|
||||
}
|
||||
|
||||
override fun onSendMessage(text: CharSequence) {
|
||||
// text contains markdown.
|
||||
onSendMessage(text.toString())
|
||||
}
|
||||
|
||||
override fun onAddAttachment() {
|
||||
onAddAttachment()
|
||||
}
|
||||
|
||||
override fun onExpandOrCompactChange() {
|
||||
}
|
||||
|
||||
override fun onFullScreenModeChanged() {
|
||||
onFullscreenToggle()
|
||||
}
|
||||
}
|
||||
setFullScreen(fullscreen, animated = false, manageKeyboard = true)
|
||||
(this as MessageComposerView).apply {
|
||||
setup(isInDarkMode, composerMode)
|
||||
}
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
// View's been inflated or state read in this block has been updated
|
||||
// Add logic here if necessary
|
||||
|
||||
// As selectedItem is read here, AndroidView will recompose
|
||||
// whenever the state changes
|
||||
// Example of Compose -> View communication
|
||||
val messageComposerView = (view as MessageComposerView)
|
||||
view.setFullScreen(fullscreen, animated = false, manageKeyboard = false)
|
||||
messageComposerView.renderComposerMode(composerMode)
|
||||
messageComposerView.sendButton.isInvisible = !composerCanSendMessage
|
||||
messageComposerView.setTextIfDifferent(composerText ?: "")
|
||||
messageComposerView.editText.requestFocus()
|
||||
val text = composerText.orEmpty()
|
||||
Row(modifier.padding(
|
||||
horizontal = 12.dp,
|
||||
vertical = 8.dp
|
||||
), verticalAlignment = Alignment.Bottom) {
|
||||
AttachmentButton(onClick = onAddAttachment, modifier = Modifier.padding(vertical = 6.dp))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
var lineCount by remember { mutableStateOf(0) }
|
||||
val roundedCorners = remember(lineCount, composerMode) {
|
||||
if (lineCount > 1 || composerMode is MessageComposerMode.Special) {
|
||||
RoundedCornerShape(20.dp)
|
||||
} else {
|
||||
RoundedCornerShape(28.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val minHeight = 42.dp
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(roundedCorners)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, roundedCorners)
|
||||
) {
|
||||
if (composerMode is MessageComposerMode.Special) {
|
||||
ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode)
|
||||
}
|
||||
val defaultTypography = ElementTextStyles.Regular.callout.copy(textAlign = TextAlign.Start)
|
||||
Box {
|
||||
BasicTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeight)
|
||||
.focusRequester(focusRequester),
|
||||
value = text,
|
||||
onValueChange = { onComposerTextChange(it) },
|
||||
onTextLayout = {
|
||||
lineCount = it.lineCount
|
||||
},
|
||||
textStyle = defaultTypography.copy(color = MaterialTheme.colorScheme.primary),
|
||||
cursorBrush = SolidColor(LocalColors.current.accentColor),
|
||||
decorationBox = { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = text,
|
||||
innerTextField = innerTextField,
|
||||
enabled = true,
|
||||
singleLine = false,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
shape = roundedCorners,
|
||||
contentPadding = PaddingValues(top = 10.dp, bottom = 10.dp, start = 12.dp, end = 42.dp),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
placeholder = {
|
||||
Text(stringResource(StringR.string.common_message), style = defaultTypography)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
unfocusedTextColor = MaterialTheme.colorScheme.secondary,
|
||||
focusedTextColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedPlaceholderColor = MaterialTheme.colorScheme.secondary,
|
||||
focusedPlaceholderColor = MaterialTheme.colorScheme.secondary,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
errorIndicatorColor = Color.Transparent,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
errorContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SendButton(
|
||||
text = text,
|
||||
canSendMessage = composerCanSendMessage,
|
||||
onSendMessage = onSendMessage,
|
||||
composerMode = composerMode,
|
||||
modifier = Modifier.padding(end = 6.dp, bottom = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FakeComposer(
|
||||
private fun ComposerModeView(
|
||||
composerMode: MessageComposerMode,
|
||||
onResetComposerMode: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// AndroidView is not Available in this mode, just render a Text
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center),
|
||||
textAlign = TextAlign.Center,
|
||||
text = "Composer Preview",
|
||||
fontSize = 20.sp,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
when (composerMode) {
|
||||
is MessageComposerMode.Edit -> {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)) {
|
||||
Icon(
|
||||
resourceId = VectorIcons.Edit,
|
||||
contentDescription = stringResource(R.string.editing),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.editing),
|
||||
style = ElementTextStyles.Regular.caption2,
|
||||
textAlign = TextAlign.Start,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(StringR.string.action_close),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clickable(
|
||||
enabled = true,
|
||||
onClick = onResetComposerMode,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
indication = rememberRipple(bounded = false)
|
||||
),
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageComposerView.setup(isDarkMode: Boolean, composerMode: MessageComposerMode) {
|
||||
val editTextColor = if (isDarkMode) {
|
||||
Color.WHITE
|
||||
} else {
|
||||
Color.BLACK
|
||||
@Composable
|
||||
private fun AttachmentButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(modifier) {
|
||||
Surface(
|
||||
Modifier
|
||||
.size(30.dp)
|
||||
.clickable(true, onClick = onClick),
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(12.5f.dp),
|
||||
painter = painterResource(R.drawable.ic_add_attachment),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Inside,
|
||||
colorFilter = ColorFilter.tint(
|
||||
LocalContentColor.current
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.SendButton(
|
||||
text: String,
|
||||
canSendMessage: Boolean,
|
||||
onSendMessage: (String) -> Unit,
|
||||
composerMode: MessageComposerMode,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val interactionSource = MutableInteractionSource()
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.background(if (canSendMessage) LocalColors.current.accentColor else Color.Transparent)
|
||||
.size(30.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
.applyIf(composerMode !is MessageComposerMode.Edit, ifTrue = {
|
||||
padding(start = 1.dp) // Center the arrow in the circle
|
||||
})
|
||||
.clickable(
|
||||
enabled = canSendMessage,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(bounded = false),
|
||||
onClick = {
|
||||
onSendMessage(text)
|
||||
}),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val iconId = when (composerMode) {
|
||||
is MessageComposerMode.Edit -> R.drawable.ic_tick
|
||||
else -> R.drawable.ic_send
|
||||
}
|
||||
val contentDescription = when (composerMode) {
|
||||
is MessageComposerMode.Edit -> stringResource(StringR.string.action_edit)
|
||||
else -> stringResource(StringR.string.action_send)
|
||||
}
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
resourceId = iconId,
|
||||
contentDescription = contentDescription,
|
||||
tint = if (canSendMessage) Color.White else LocalColors.current.quaternary
|
||||
)
|
||||
}
|
||||
editText.setTextColor(editTextColor)
|
||||
editText.setHintTextColor(editTextColor)
|
||||
editText.setHint(R.string.rich_text_editor_composer_placeholder)
|
||||
emojiButton?.isVisible = true
|
||||
sendButton.isVisible = true
|
||||
editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED
|
||||
renderComposerMode(composerMode)
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
@ -162,15 +297,38 @@ internal fun TextComposerDarkPreview() = ElementPreviewDark { ContentToPreview()
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TextComposer(
|
||||
onSendMessage = {},
|
||||
fullscreen = false,
|
||||
onFullscreenToggle = { },
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Normal(""),
|
||||
onCloseSpecialMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "Message",
|
||||
isInDarkMode = true,
|
||||
)
|
||||
Column {
|
||||
TextComposer(
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Normal(""),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = false,
|
||||
composerText = "",
|
||||
)
|
||||
TextComposer(
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Normal(""),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message",
|
||||
)
|
||||
TextComposer(
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Normal(""),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
|
||||
)
|
||||
TextComposer(
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text"),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.textcomposer.tools
|
||||
|
||||
import android.text.Spanned
|
||||
import android.widget.EditText
|
||||
|
||||
fun EditText.setTextIfDifferent(newText: CharSequence?): Boolean {
|
||||
if (!isTextDifferent(newText, text)) {
|
||||
// Previous text is the same. No op
|
||||
return false
|
||||
}
|
||||
setText(newText)
|
||||
// Since the text changed we move the cursor to the end of the new text.
|
||||
// This allows us to fill in text programmatically with a different value,
|
||||
// but if the user is typing and the view is rebound we won't lose their cursor position.
|
||||
setSelection(newText?.length ?: 0)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isTextDifferent(str1: CharSequence?, str2: CharSequence?): Boolean {
|
||||
if (str1 === str2) {
|
||||
return false
|
||||
}
|
||||
if (str1 == null || str2 == null) {
|
||||
return true
|
||||
}
|
||||
val length = str1.length
|
||||
if (length != str2.length) {
|
||||
return true
|
||||
}
|
||||
if (str1 is Spanned) {
|
||||
return str1 != str2
|
||||
}
|
||||
for (i in 0 until length) {
|
||||
if (str1[i] != str2[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.textcomposer.tools
|
||||
|
||||
import androidx.transition.Transition
|
||||
|
||||
open class SimpleTransitionListener : Transition.TransitionListener {
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onTransitionResume(transition: Transition) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onTransitionPause(transition: Transition) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onTransitionCancel(transition: Transition) {
|
||||
// No op
|
||||
}
|
||||
|
||||
override fun onTransitionStart(transition: Transition) {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.textcomposer.tools
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.transition.ChangeBounds
|
||||
import androidx.transition.Fade
|
||||
import androidx.transition.Transition
|
||||
import androidx.transition.TransitionManager
|
||||
import androidx.transition.TransitionSet
|
||||
|
||||
fun ViewGroup.animateLayoutChange(animationDuration: Long, transitionComplete: (() -> Unit)? = null) {
|
||||
val transition = TransitionSet().apply {
|
||||
ordering = TransitionSet.ORDERING_SEQUENTIAL
|
||||
addTransition(ChangeBounds())
|
||||
addTransition(Fade(Fade.IN))
|
||||
duration = animationDuration
|
||||
addListener(object : SimpleTransitionListener() {
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
transitionComplete?.invoke()
|
||||
}
|
||||
})
|
||||
}
|
||||
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,773Q457.91,773 441.46,756.54Q425,740.09 425,718.31L425,535L241.69,535Q219.91,535 203.46,518.54Q187,502.09 187,480Q187,457.91 203.46,441.46Q219.91,425 241.69,425L425,425L425,241.69Q425,219.91 441.46,203.46Q457.91,187 480,187Q502.09,187 518.54,203.46Q535,219.91 535,241.69L535,425L718.31,425Q740.09,425 756.54,441.46Q773,457.91 773,480Q773,502.09 756.54,518.54Q740.09,535 718.31,535L535,535L535,718.31Q535,740.09 518.54,756.54Q502.09,773 480,773Z"/>
|
||||
</vector>
|
||||
9
libraries/textcomposer/src/main/res/drawable/ic_send.xml
Normal file
9
libraries/textcomposer/src/main/res/drawable/ic_send.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M15.404,8.965L1.563,15.882C0.631,16.348 -0.34,15.348 0.116,14.435C0.116,14.435 1.832,10.971 2.303,10.064C2.775,9.156 3.315,8.999 8.331,8.351C8.517,8.327 8.669,8.187 8.669,8C8.669,7.813 8.517,7.673 8.331,7.649C3.315,7.001 2.775,6.844 2.303,5.936C1.832,5.029 0.116,1.565 0.116,1.565C-0.34,0.653 0.631,-0.348 1.563,0.118L15.404,7.036C16.199,7.433 16.199,8.567 15.404,8.965Z"
|
||||
android:fillColor="#A6ADB7"/>
|
||||
</vector>
|
||||
9
libraries/textcomposer/src/main/res/drawable/ic_tick.xml
Normal file
9
libraries/textcomposer/src/main/res/drawable/ic_tick.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="15dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="15">
|
||||
<path
|
||||
android:pathData="M6.518,14.779C6.953,14.779 7.297,14.597 7.535,14.233L15.403,1.968C15.579,1.692 15.65,1.461 15.65,1.234C15.65,0.657 15.245,0.26 14.662,0.26C14.249,0.26 14.009,0.399 13.759,0.792L6.484,12.348L2.736,7.529C2.492,7.205 2.236,7.07 1.874,7.07C1.277,7.07 0.857,7.489 0.857,8.066C0.857,8.315 0.95,8.565 1.158,8.819L5.495,14.245C5.784,14.606 6.096,14.779 6.518,14.779Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
|
|
@ -98,6 +98,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
|||
implementation(project(":libraries:mediapickers:impl"))
|
||||
implementation(project(":libraries:mediaupload:impl"))
|
||||
implementation(project(":libraries:usersearch:impl"))
|
||||
implementation(project(":libraries:textcomposer"))
|
||||
}
|
||||
|
||||
fun DependencyHandlerScope.allServicesImpl() {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d941ac36664583640991d6a310edf45b4f87d8285647d6aa661970dba3c365b4
|
||||
size 83119
|
||||
oid sha256:8296d9d11de5cc99531910ca7252377e084234b09dfb49e1f5b9fa2d1ce602cf
|
||||
size 83194
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:99f4447da79ca0c37c7607fb673b6dfd29cb241ba30420adcc0399db170a5810
|
||||
size 98150
|
||||
oid sha256:5c0668727ef819f0f5d2107ce3b6210110879d616c7aeda7d87e54fee1ec6f89
|
||||
size 99242
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:12839404b6e9eaf8983ccf3e5773cd8c80174068e76924ad72b7b6429018b0b6
|
||||
size 79478
|
||||
oid sha256:e556931f594409bbf654dd410a25e418935bb8ffd5b7a8eea429a69b0c05424c
|
||||
size 79592
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4115bdacc08b7c5bdb4c2859d131eaa0e84748d3f42e2de388ffef72131851a2
|
||||
size 93105
|
||||
oid sha256:048e556d97ea6301fdb6991776fd4f089e56e227d4babb60c73f350ed97b45a9
|
||||
size 94026
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0d7a3255202968ab6a467b698ca41154b4564882ac3e45578ea36060a8c97583
|
||||
size 61586
|
||||
oid sha256:55d18860fbd298703c4f2345fea13bb6edaa065727aed4f043d6ed571a5d091c
|
||||
size 61509
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0bab25b90e451021883fcd388030ac054729d1ea28a2b4e9b6041f5a103f0a7b
|
||||
size 99914
|
||||
oid sha256:68b9ca77722425893f3f28ab5df3770409b5b3a56b64eeae08e7c82d9a1e3e80
|
||||
size 99857
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1a3a36f97c7c22d4113570ce8413de29c1d62e96d1d979851a66f051ca229e08
|
||||
size 59148
|
||||
oid sha256:e3b2315704405e618e19f6d3e8d6d19ce0564e621b81a6a78e157ac1b2a0487b
|
||||
size 59131
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b28cfcb8e238559bd7775507397307fef4e192bb9e1615a0b08f048e73a5e6dd
|
||||
size 96054
|
||||
oid sha256:808627e2b8e737b673a03a505e671a96e58ead5b9ccf896ad71e802346dac638
|
||||
size 96033
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:88f8d524d6a22a05b013c8cdc73b909a4ca41938d0fdb41923381c2045e6b37f
|
||||
size 8990
|
||||
oid sha256:edfafd24c8085ba7fe218c720b9dc2fead18d729b6ddea3b9fc04b51cb7c0e91
|
||||
size 9418
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f6ff5a856c3af2b85848a80e7d832ebbf737e7d5cf06105d5a01c1e6b3f1eeb7
|
||||
size 9115
|
||||
oid sha256:f6b92a6d4f1a2287cefe06c6ba7cb2a2210d61cf4c38747ffd308e4455d78f71
|
||||
size 9335
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e20ba9c18742b91164f5d0bdbbfe41b3960ab67bf921c2d305cfd72fbe57e52
|
||||
size 33100
|
||||
oid sha256:596e4e45b76ab154f3b4011a1ef3fbd27ae801a758949c7a901262c752e7eb6e
|
||||
size 33461
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3dfa909234ae27da447b12391d53657afd23ef0dd17cff4c8bb55ef1b9811432
|
||||
size 38347
|
||||
oid sha256:46cb0ed395274b4201da2bc604695e2cd805ad74cff58cc1ba341d38cb63dd6f
|
||||
size 38863
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c1b9dc41e7f4abce90208ce21b2537d8522e1467afdc702735f2d3ab79ed265
|
||||
size 38320
|
||||
oid sha256:84aae5ef0c7c9d8308e56b290cdbab809e6e732181fbcbcb22be794d8cb2f34b
|
||||
size 38780
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d9f210f49c4b9a17b01fffe23229afd6dc7909a50ff79732c4d7f169f6c6c558
|
||||
size 18098
|
||||
oid sha256:0d59f1042dfdeb97fb2992c21c2c50a6ae1f8535a5b72063c874a91c1ece6076
|
||||
size 18355
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4782d54f26127fa3e8eb2674486c093036310febc29fc883731eddab158cad40
|
||||
size 33261
|
||||
oid sha256:26b409cd4f3c950458e12b1ae3dcd586705edf6f2d79a549aab8b84b59578314
|
||||
size 33578
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d0741f10b79b3ba739a7d2f160e61df8ea30c7e90d5b03db66cf0fadae39637d
|
||||
size 31877
|
||||
oid sha256:c4a7984c30e35b3b1641002b5facfe08dfab34e18c12383f82ae479c5ab57e25
|
||||
size 32435
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f9e818b80358ed35dfe0b1b1d26b8e64a137aa6f2fbce02385cb3cc7900bd852
|
||||
size 37293
|
||||
oid sha256:ff299a00cc2b4006fae62e24765a2810fcca23cf56e4b8df86b09e8588a96dc4
|
||||
size 37631
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:18dd6a307f1990b95cddba7e5ae199fc2c9746fdc49231ceb320558707c1ec1c
|
||||
size 37252
|
||||
oid sha256:9942eefb85c7525fe17184efa0b5dae53d7b1d76eff618b7ff99e90ae7220893
|
||||
size 37632
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c0e24c4fc71151ae8dc04a6213ecc15e0d25dbe6b1684d177086572aeff84c5
|
||||
size 17289
|
||||
oid sha256:074e0273e41666a37e1c2d0887956a9949d84364518e89ae31cb2523f1c3b37d
|
||||
size 17417
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:49048bedc855a3460f7a3e7c387481c53e4633ef4110f120c5f8ce649980a583
|
||||
size 32034
|
||||
oid sha256:721007e228a1fac6c5e61f6a17ae5d2871fe6515a3f351a4ec91214a5b4e4538
|
||||
size 32602
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4bddb1e4c74494ed89812a31e35c8d056a3109043fa817c1dbae13d522e56566
|
||||
size 66641
|
||||
oid sha256:8340785a1a871d93d9acda9d788d63c91b8270c747bb7b7748b7e532f56ed7e5
|
||||
size 66547
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4d6e5e784a6ee50cbf767bc0da43b493e910ee2a7ace9890419916d23c1e11a3
|
||||
size 45461
|
||||
oid sha256:b3b668c53fbb814eb7c6c3a37f068e9475e0dd286a1099581e17c41b806cfa31
|
||||
size 48090
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f74cb8516eaf4d8f0e289f685d520bc130df21cbfc4fbc68eed001c4138c83b2
|
||||
size 45907
|
||||
oid sha256:43f0d1fbafcde1e07b8ea58a38505916d7aa987aac8d9f3aef323f9001a80344
|
||||
size 48525
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:360c2c69a3d03538f6f61847757a15e88051b38195cadcd33f9cf2e57638b3f1
|
||||
size 41761
|
||||
oid sha256:8662ededaa1c6682d2571e3398d6007dcf92a392efc59985c64bd48d68e2c296
|
||||
size 43088
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:360c2c69a3d03538f6f61847757a15e88051b38195cadcd33f9cf2e57638b3f1
|
||||
size 41761
|
||||
oid sha256:8662ededaa1c6682d2571e3398d6007dcf92a392efc59985c64bd48d68e2c296
|
||||
size 43088
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:75f493a62614704c956b8be9351532bb431cc69e400adc9f1d536579b6dc731c
|
||||
size 64226
|
||||
oid sha256:66a2c37dbb7478d45546d8ae404befe0ed0efa7c3ff467566fc4b4b9b76b7628
|
||||
size 64363
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e96dae6cc767476bd60f23079776600449414a9423976b85c91c348ed065d7ae
|
||||
size 44877
|
||||
oid sha256:8219ba85624c3bdc6671b2135aa04d16597fd4087ed38456769c161471984012
|
||||
size 47443
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fd0bb44da30359d50cf5754768d58d75bf4489c9d6fe9c4d5c0d2f3fd9020541
|
||||
size 45302
|
||||
oid sha256:6c0dfdc6684fece8358de955497762f43affd44fc44d35ce8982b2e713b0006f
|
||||
size 47873
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9654f73bd95d1b0d03dc6ea44d13c4910fb8e64a3c86644a11d5bc8591a4be8a
|
||||
size 41312
|
||||
oid sha256:352cce29147cc831da72f7d0a5948d3d0841e05c168f86a09492fa6ab44942e9
|
||||
size 42405
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9654f73bd95d1b0d03dc6ea44d13c4910fb8e64a3c86644a11d5bc8591a4be8a
|
||||
size 41312
|
||||
oid sha256:352cce29147cc831da72f7d0a5948d3d0841e05c168f86a09492fa6ab44942e9
|
||||
size 42405
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee403297e15204e165ec5dce90e3e93e95e8e1b2e2cbd03489b91c4c9a6de083
|
||||
size 10360
|
||||
oid sha256:34d59f799c43ffcaa65b5af54e35a7507376db666093c4d5698d4125bd1e2228
|
||||
size 10796
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:33237fb8f262176e6f291a8ded90ab56600113c93bed188f6d4d01df950df53e
|
||||
size 9286
|
||||
oid sha256:36b3bc2c243a8c09bbdba33f617a6d0fdbb0f5943560e0f23008cc129d1b5682
|
||||
size 9520
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:22afae6cd16ea0134b0966e5fbd7003592b9133e3d57947f062b7c9ebd4694d4
|
||||
size 33717
|
||||
oid sha256:14286b8d22070f428630ae1d4244da42f33d4aa4906fbcc09cae4a034ded7707
|
||||
size 33693
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:68e175fb1c070b17a2f7ef6644837fb37b906b122df737bb9740aa591aaddd34
|
||||
size 32943
|
||||
oid sha256:e30d0f0aff589c97e152b48d701a66ff323ab74aa1215af8dc1962bbff1a7204
|
||||
size 32867
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a48ccf1ff3570915b0673de7c0367d951b795c833d7896645b37000b8dc09c15
|
||||
size 9649
|
||||
oid sha256:8c88bfa85e91e6ab247e3feec199c8a29233827dbd2aad28cf9ff00ea9b99117
|
||||
size 10927
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:945b360a62877d16ec7c25942158ae47e244df85118ca861482ff3bbf0aef55f
|
||||
size 9268
|
||||
oid sha256:36adfe10149d33ce817d520d3f542b5441713f7ceddc5864747f624f5e26d432
|
||||
size 10620
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce6cbcf47c5e76d72fd9bf15e5676510ac29a3087cc862c67ccd4f7de1f24359
|
||||
size 45546
|
||||
oid sha256:7fbb93faeae669493b42bc5831b1c5bb67b25ed688d582e01249a9e23e03425a
|
||||
size 47340
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9af27bab7bbac9914f4e2694f1ea9c200c57edb345bd107cfb5c2edb48cd056e
|
||||
size 45725
|
||||
oid sha256:e5e5ac66335825c321666fe9564d1dbbd00ebbfe67f39d89ac133fcbd2b36b70
|
||||
size 47473
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce6cbcf47c5e76d72fd9bf15e5676510ac29a3087cc862c67ccd4f7de1f24359
|
||||
size 45546
|
||||
oid sha256:72bc2d1171ca0d80bfc4522bbbb15c5973dbae362351c344f9b25382da4831d0
|
||||
size 47701
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:99e4c0fe8076952a490ab58807913984a9a6598b589c5e35bc9eececc741bf11
|
||||
size 44666
|
||||
oid sha256:603a9bc343feceb5c1f86a36dcfdca7edf71a150237419a16895d12247b032d2
|
||||
size 46271
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:46660a369ee182afbf49c5d8022fa79c4a358aee7e2aea83bdf08f66a16b5ff1
|
||||
size 45095
|
||||
oid sha256:b06f5cdf95f2850a488e71c24c9857eca7b62d147b46b77b3c2ffece75789491
|
||||
size 46682
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:99e4c0fe8076952a490ab58807913984a9a6598b589c5e35bc9eececc741bf11
|
||||
size 44666
|
||||
oid sha256:b4a13a0332f1d5ce24824434cb909de2d2bf4af601e6924155d2a550d9e03ec0
|
||||
size 46634
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:18569af3c955f9280122faf1ac653af2f569ee54bfa6d6d74b83adb2813ee04e
|
||||
size 35446
|
||||
oid sha256:036d832c19fcba93b8848f9687f4029eba0beeb56893b740062b18c769f419cd
|
||||
size 35424
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:196cff5b2ee404d5547803039a919f565058a9a0b4c586b05458b63b37b54fd5
|
||||
size 28153
|
||||
oid sha256:22eb3320d95bb28b621ff2e7b3421f79b539d84a12591c885f088b3ae404b209
|
||||
size 28130
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05fc7b6fa4e71ae48e795422184bcefd809c568f62a0709d08ffca045f078e66
|
||||
size 55574
|
||||
oid sha256:55ae80019f919603ec36672b0cc1f0571a9b9135994121fcc2b3420ffc667c40
|
||||
size 55567
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:119ae9d67a9614e8b461d3a84dac8a59dfa59a72bdee5e178fe317cf2717f5d6
|
||||
size 34823
|
||||
oid sha256:b423c2a214c752e980b14181df543f178bf3aa878b0999802cee35d0a79cfb73
|
||||
size 34801
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:569a281b9ceeadebbf84572aa6f124ccacf8e82d88102b7e7af627350c252afb
|
||||
size 34453
|
||||
oid sha256:a02bb25e5802305dcd08aa092f7201fb6e1e59c6e8dc8b6760756b75786de82f
|
||||
size 34433
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f659dc9210a8165e992efa8b2884535417be7e4b35e83b1fc44b76fab9eeb2a7
|
||||
size 29956
|
||||
oid sha256:4ced1625c98be58c1fe1c4f29dc2e4df78abfabda0276274616f09dfefa65f8f
|
||||
size 30517
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e3c1ac7bb831e3fc9d9ac8500849342095a4a35dff2ee12879051e9bffb73894
|
||||
size 35541
|
||||
oid sha256:16db29fe1b26c978613cacadc059bd8d01796785d96444142cd14415154ea1d2
|
||||
size 35521
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4603362c3c2dfa9e002eb64c14eb0969ff749a4f0315e2b8810a58253ae42d21
|
||||
size 27992
|
||||
oid sha256:dac8c13ec5b3086829d50d92efc8cece7e60ffcb475cc01c0a8ccfc3069ae4e7
|
||||
size 27973
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f199a3afe2771b33953e56af4f30f6ece7bd8ae421efd3b5601f6393c2d07617
|
||||
size 56336
|
||||
oid sha256:fbf110969859056024143b3665ddf735e0c772b4706890db194071107910199e
|
||||
size 56316
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72847de27dbb5a0126b90c388b14ef443838c5832e8a37f1d29f51b2eb404d97
|
||||
size 34086
|
||||
oid sha256:9e45a51563799bd8ca32d56154819cd8df0fa6a466ae48ed809e3e40a2acd3b1
|
||||
size 34064
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e227432ad7515e2f060ed665861d65f502c354e700fcc428faa6ef20717c56de
|
||||
size 34100
|
||||
oid sha256:45c113cdecc92a2210df1bc595159b0a02a115d527bf5f9344b1e177147d6770
|
||||
size 34076
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7df2eb3bc330ca400199bcf233914b91bd37e83ea6fd6bc9ac2dfe81102b85ed
|
||||
size 30137
|
||||
oid sha256:10bcb62743748e2e04ceaf5e82a70ecf42f7361fa88ce0d7d63079ee45ce5209
|
||||
size 30155
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6ce332185060fe0571610e9a060ce6dfafc7619dad384725456a4a755c39a123
|
||||
size 61131
|
||||
oid sha256:9e744f45c0a29e3cdc6d0d5417b9702b1a580c557e6d280e046b4a29319bb11f
|
||||
size 61392
|
||||
|
|
|
|||
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