Merge branch 'develop' into feature/fga/media_viewer_actions
This commit is contained in:
commit
8a3a70ffa5
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
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.repository == 'vector-im/element-x-android' }}
|
if: ${{ github.repository == 'vector-im/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: ⏬ Checkout with LFS
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
lfs: 'true'
|
||||||
|
|
||||||
- name: Use JDK 17
|
- name: Use JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
|
|
@ -41,3 +45,27 @@ jobs:
|
||||||
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
|
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
|
||||||
if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
|
if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
|
||||||
run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
|
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 }}
|
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
|
||||||
# Fallback for forks
|
# Fallback for forks
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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)}
|
- inputText: ${ROOM_NAME.substring(0, 3)}
|
||||||
- takeScreenshot: build/maestro/400-SearchRoom
|
- takeScreenshot: build/maestro/400-SearchRoom
|
||||||
- tapOn: ${ROOM_NAME}
|
- tapOn: ${ROOM_NAME}
|
||||||
# Close keyboard
|
|
||||||
- hideKeyboard
|
|
||||||
# Back from timeline
|
# Back from timeline
|
||||||
- back
|
- back
|
||||||
# Close keyboard
|
# Close keyboard
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@ appId: ${APP_ID}
|
||||||
# TODO Create a room on a new account
|
# TODO Create a room on a new account
|
||||||
- tapOn: ${ROOM_NAME}
|
- tapOn: ${ROOM_NAME}
|
||||||
- takeScreenshot: build/maestro/500-Timeline
|
- takeScreenshot: build/maestro/500-Timeline
|
||||||
- tapOn: "Message…"
|
- tapOn: "Message"
|
||||||
- inputText: "Hello world!"
|
- inputText: "Hello world!"
|
||||||
- tapOn: "Toggle full screen mode"
|
|
||||||
- tapOn: "Toggle full screen mode"
|
|
||||||
- tapOn: "Send"
|
- tapOn: "Send"
|
||||||
- hideKeyboard
|
- hideKeyboard
|
||||||
- back
|
- back
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,8 @@ dependencies {
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
|
|
||||||
implementation(platform(libs.network.okhttp.bom))
|
implementation(platform(libs.network.okhttp.bom))
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
implementation(libs.network.okhttp.logging)
|
||||||
|
implementation(libs.serialization.json)
|
||||||
|
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
kapt(libs.dagger.compiler)
|
kapt(libs.dagger.compiler)
|
||||||
|
|
|
||||||
|
|
@ -158,10 +158,13 @@ koverMerged {
|
||||||
"anvil.hint.merge.*",
|
"anvil.hint.merge.*",
|
||||||
"anvil.module.*",
|
"anvil.module.*",
|
||||||
"com.airbnb.android.showkase*",
|
"com.airbnb.android.showkase*",
|
||||||
|
"io.element.android.libraries.designsystem.showkase.*",
|
||||||
"*_Factory",
|
"*_Factory",
|
||||||
"*_Factory$*",
|
"*_Factory$*",
|
||||||
"*_Module",
|
"*_Module",
|
||||||
"*_Module$*",
|
"*_Module$*",
|
||||||
|
"*Module_Provides*",
|
||||||
|
"Dagger*Component*",
|
||||||
"*ComposableSingletons$*",
|
"*ComposableSingletons$*",
|
||||||
"*_AssistedFactory_Impl*",
|
"*_AssistedFactory_Impl*",
|
||||||
"*BuildConfig",
|
"*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.
|
// 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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
.padding(16.dp)
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
verticalAlignment = Alignment.Top
|
verticalAlignment = Alignment.Top
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.attachments.preview
|
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
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.attachments.preview.error.sendAttachmentError
|
||||||
import io.element.android.features.messages.impl.media.local.LocalMediaView
|
import io.element.android.features.messages.impl.media.local.LocalMediaView
|
||||||
import io.element.android.libraries.architecture.Async
|
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.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
|
@ -151,9 +147,8 @@ private fun AttachmentsPreviewBottomActions(
|
||||||
onSendClicked: () -> Unit,
|
onSendClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
ButtonRowMolecule(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = onCancelClicked) {
|
TextButton(onClick = onCancelClicked) {
|
||||||
Text(stringResource(id = StringsR.string.action_cancel))
|
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 androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.textcomposer.TextComposer
|
import io.element.android.libraries.textcomposer.TextComposer
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -52,17 +51,14 @@ fun MessageComposerView(
|
||||||
|
|
||||||
TextComposer(
|
TextComposer(
|
||||||
onSendMessage = ::sendMessage,
|
onSendMessage = ::sendMessage,
|
||||||
fullscreen = state.isFullScreen,
|
|
||||||
onFullscreenToggle = ::onFullscreenToggle,
|
|
||||||
composerMode = state.mode,
|
composerMode = state.mode,
|
||||||
onCloseSpecialMode = ::onCloseSpecialMode,
|
onResetComposerMode = ::onCloseSpecialMode,
|
||||||
onComposerTextChange = ::onComposerTextChange,
|
onComposerTextChange = ::onComposerTextChange,
|
||||||
onAddAttachment = {
|
onAddAttachment = {
|
||||||
state.eventSink(MessageComposerEvents.AddAttachment)
|
state.eventSink(MessageComposerEvents.AddAttachment)
|
||||||
},
|
},
|
||||||
composerCanSendMessage = state.isSendButtonVisible,
|
composerCanSendMessage = state.isSendButtonVisible,
|
||||||
composerText = state.text?.charSequence?.toString(),
|
composerText = state.text?.charSequence?.toString(),
|
||||||
isInDarkMode = !ElementTheme.colors.isLight,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.onboarding.impl
|
package io.element.android.features.onboarding.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.atomic.pages.OnBoardingPage
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -117,12 +117,7 @@ private fun OnBoardingButtons(
|
||||||
onCreateAccount: () -> Unit,
|
onCreateAccount: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
ButtonColumnMolecule(modifier = modifier) {
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalAlignment = CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
if (state.canLoginWithQrCode) {
|
if (state.canLoginWithQrCode) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,7 @@ internal fun TopicSection(
|
||||||
Text(
|
Text(
|
||||||
roomTopic.topic,
|
roomTopic.topic,
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
|
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
|
color = MaterialTheme.colorScheme.tertiary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,7 @@ private fun LabelledReadOnlyField(
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
text = title,
|
text = title,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
|
@ -218,32 +217,34 @@ fun RoomListContent(
|
||||||
|
|
||||||
if (state.invitesState != InvitesState.NoInvites) {
|
if (state.invitesState != InvitesState.NoInvites) {
|
||||||
item {
|
item {
|
||||||
Row(
|
Box(
|
||||||
horizontalArrangement = Arrangement.End,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
|
||||||
.heightIn(min = 48.dp),
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
text = stringResource(StringR.string.action_invites_list),
|
modifier = Modifier
|
||||||
fontSize = 14.sp,
|
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
||||||
style = noFontPadding,
|
.padding(horizontal = 16.dp)
|
||||||
)
|
.align(Alignment.CenterEnd)
|
||||||
|
.heightIn(min = 48.dp),
|
||||||
if (state.invitesState == InvitesState.NewInvites) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
Spacer(Modifier.width(8.dp))
|
) {
|
||||||
|
Text(
|
||||||
Box(
|
text = stringResource(StringR.string.action_invites_list),
|
||||||
modifier = Modifier
|
fontSize = 14.sp,
|
||||||
.size(12.dp)
|
style = noFontPadding,
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.roomListUnreadIndicator())
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
package io.element.android.features.verifysession.impl
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -38,25 +34,21 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
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.components.button.ButtonWithProgress
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
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.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.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||||
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep
|
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep
|
||||||
|
|
@ -82,21 +74,18 @@ fun VerifySelfSessionView(
|
||||||
val buttonsVisible by remember(verificationFlowStep) {
|
val buttonsVisible by remember(verificationFlowStep) {
|
||||||
derivedStateOf { verificationFlowStep != FlowStep.AwaitingOtherDeviceResponse && verificationFlowStep != FlowStep.Completed }
|
derivedStateOf { verificationFlowStep != FlowStep.AwaitingOtherDeviceResponse && verificationFlowStep != FlowStep.Completed }
|
||||||
}
|
}
|
||||||
Surface {
|
HeaderFooterPage(
|
||||||
Column(modifier = modifier.systemBarsPadding()) {
|
modifier = modifier,
|
||||||
Column(
|
header = {
|
||||||
modifier = Modifier
|
HeaderContent(verificationFlowStep = verificationFlowStep)
|
||||||
.fillMaxWidth()
|
},
|
||||||
.weight(1f)
|
footer = {
|
||||||
.padding(horizontal = 20.dp)
|
|
||||||
) {
|
|
||||||
HeaderContent(verificationFlowStep = verificationFlowStep)
|
|
||||||
Content(modifier = Modifier.weight(1f), flowState = verificationFlowStep)
|
|
||||||
}
|
|
||||||
if (buttonsVisible) {
|
if (buttonsVisible) {
|
||||||
BottomMenu(screenState = state, goBack = ::goBackAndCancelIfNeeded)
|
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
|
FlowStep.Ready, is FlowStep.Verifying, FlowStep.Completed -> R.string.screen_session_verification_compare_emojis_subtitle
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier) {
|
IconTitleSubtitleMolecule(
|
||||||
Spacer(Modifier.height(80.dp))
|
modifier = modifier.padding(top = 60.dp),
|
||||||
Box(
|
iconResourceId = iconResourceId,
|
||||||
modifier = Modifier
|
title = stringResource(id = titleTextId),
|
||||||
.size(width = 70.dp, height = 70.dp)
|
subTitle = stringResource(id = subtitleTextId)
|
||||||
.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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun Content(flowState: FlowStep, modifier: Modifier = Modifier) {
|
internal fun Content(flowState: FlowStep, modifier: Modifier = Modifier) {
|
||||||
Column(modifier, verticalArrangement = Arrangement.Center) {
|
Column(modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
|
||||||
Spacer(Modifier.shrinkableHeight(min = 20.dp, max = 56.dp))
|
|
||||||
when (flowState) {
|
when (flowState) {
|
||||||
FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit
|
FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit
|
||||||
FlowStep.AwaitingOtherDeviceResponse -> ContentWaiting()
|
FlowStep.AwaitingOtherDeviceResponse -> ContentWaiting()
|
||||||
is FlowStep.Verifying -> ContentVerifying(flowState)
|
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
|
else -> goBack
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
ButtonColumnMolecule(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(bottom = 20.dp)
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 20.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
) {
|
||||||
ButtonWithProgress(
|
ButtonWithProgress(
|
||||||
text = positiveButtonTitle?.let { stringResource(it) },
|
text = positiveButtonTitle?.let { stringResource(it) },
|
||||||
|
|
@ -272,7 +222,6 @@ internal fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
|
||||||
onClick = { positiveButtonEvent?.let { eventSink(it) } }
|
onClick = { positiveButtonEvent?.let { eventSink(it) } }
|
||||||
)
|
)
|
||||||
if (negativeButtonTitle != null) {
|
if (negativeButtonTitle != null) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = negativeButtonCallback,
|
onClick = negativeButtonCallback,
|
||||||
|
|
@ -281,7 +230,6 @@ internal fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
|
||||||
Text(stringResource(negativeButtonTitle), fontSize = 16.sp)
|
Text(stringResource(negativeButtonTitle), fontSize = 16.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(40.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,15 +250,3 @@ private fun ContentToPreview(state: VerifySelfSessionState) {
|
||||||
goBack = {},
|
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
|
||||||
network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.11.0"
|
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 = "com.squareup.retrofit2:retrofit:2.9.0"
|
||||||
network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.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" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
anvil = { id = "com.squareup.anvil", version.ref = "anvil" }
|
anvil = { id = "com.squareup.anvil", version.ref = "anvil" }
|
||||||
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
|
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" }
|
dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" }
|
||||||
dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" }
|
dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" }
|
||||||
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" }
|
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.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
|
@ -47,6 +48,7 @@ fun LabelledTextField(
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
text = label
|
text = label
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -72,13 +72,13 @@ private fun ProgressDialogContent(
|
||||||
modifier = Modifier.padding(top = 38.dp, bottom = 32.dp, start = 40.dp, end = 40.dp)
|
modifier = Modifier.padding(top = 38.dp, bottom = 32.dp, start = 40.dp, end = 40.dp)
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
Spacer(modifier = Modifier.height(22.dp))
|
Spacer(modifier = Modifier.height(22.dp))
|
||||||
Text(
|
Text(
|
||||||
text = 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 androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
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
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -48,7 +49,7 @@ fun PreferenceCategory(
|
||||||
}
|
}
|
||||||
content()
|
content()
|
||||||
if (showDivider) {
|
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.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
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.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
|
@ -131,13 +132,13 @@ private fun ContentToPreview() {
|
||||||
subtitle = "Some other text",
|
subtitle = "Some other text",
|
||||||
icon = Icons.Default.BugReport,
|
icon = Icons.Default.BugReport,
|
||||||
)
|
)
|
||||||
PreferenceDivider()
|
Divider()
|
||||||
PreferenceSwitch(
|
PreferenceSwitch(
|
||||||
title = "Switch",
|
title = "Switch",
|
||||||
icon = Icons.Default.Announcement,
|
icon = Icons.Default.Announcement,
|
||||||
isChecked = true,
|
isChecked = true,
|
||||||
)
|
)
|
||||||
PreferenceDivider()
|
Divider()
|
||||||
PreferenceSlide(
|
PreferenceSlide(
|
||||||
title = "Slide",
|
title = "Slide",
|
||||||
summary = "Summary",
|
summary = "Summary",
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ fun PreferenceText(
|
||||||
}
|
}
|
||||||
if (subtitle != null) {
|
if (subtitle != null) {
|
||||||
Text(
|
Text(
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
text = subtitle,
|
text = subtitle,
|
||||||
color = tintColor ?: MaterialTheme.colorScheme.tertiary,
|
color = tintColor ?: MaterialTheme.colorScheme.tertiary,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ fun elementColorsDark() = ElementColors(
|
||||||
gray400 = Compound_Gray_400_Dark,
|
gray400 = Compound_Gray_400_Dark,
|
||||||
gray1400 = Compound_Gray_1400_Dark,
|
gray1400 = Compound_Gray_1400_Dark,
|
||||||
textActionCritical = TextColorCriticalDark,
|
textActionCritical = TextColorCriticalDark,
|
||||||
|
accentColor = Color(0xFF0DBD8B),
|
||||||
placeholder = Compound_Gray_800_Dark,
|
placeholder = Compound_Gray_800_Dark,
|
||||||
isLight = false,
|
isLight = false,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ fun elementColorsLight() = ElementColors(
|
||||||
gray400 = Compound_Gray_400_Light,
|
gray400 = Compound_Gray_400_Light,
|
||||||
gray1400 = Compound_Gray_1400_Light,
|
gray1400 = Compound_Gray_1400_Light,
|
||||||
textActionCritical = TextColorCriticalLight,
|
textActionCritical = TextColorCriticalLight,
|
||||||
|
accentColor = Color(0xFF0DBD8B),
|
||||||
placeholder = Compound_Gray_800_Light,
|
placeholder = Compound_Gray_800_Light,
|
||||||
isLight = true,
|
isLight = true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ class ElementColors(
|
||||||
gray400: Color,
|
gray400: Color,
|
||||||
gray1400: Color,
|
gray1400: Color,
|
||||||
textActionCritical: Color,
|
textActionCritical: Color,
|
||||||
|
accentColor: Color,
|
||||||
placeholder: Color,
|
placeholder: Color,
|
||||||
isLight: Boolean
|
isLight: Boolean
|
||||||
) {
|
) {
|
||||||
|
|
@ -61,6 +62,9 @@ class ElementColors(
|
||||||
var textActionCritical by mutableStateOf(textActionCritical)
|
var textActionCritical by mutableStateOf(textActionCritical)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var accentColor by mutableStateOf(accentColor)
|
||||||
|
private set
|
||||||
|
|
||||||
var placeholder by mutableStateOf(placeholder)
|
var placeholder by mutableStateOf(placeholder)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
@ -77,6 +81,7 @@ class ElementColors(
|
||||||
gray400: Color = this.gray400,
|
gray400: Color = this.gray400,
|
||||||
gray1400: Color = this.gray1400,
|
gray1400: Color = this.gray1400,
|
||||||
textActionCritical: Color = this.textActionCritical,
|
textActionCritical: Color = this.textActionCritical,
|
||||||
|
accentColor: Color = this.accentColor,
|
||||||
placeholder: Color = this.placeholder,
|
placeholder: Color = this.placeholder,
|
||||||
isLight: Boolean = this.isLight,
|
isLight: Boolean = this.isLight,
|
||||||
) = ElementColors(
|
) = ElementColors(
|
||||||
|
|
@ -89,6 +94,7 @@ class ElementColors(
|
||||||
gray400 = gray400,
|
gray400 = gray400,
|
||||||
gray1400 = gray1400,
|
gray1400 = gray1400,
|
||||||
textActionCritical = textActionCritical,
|
textActionCritical = textActionCritical,
|
||||||
|
accentColor = accentColor,
|
||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
isLight = isLight,
|
isLight = isLight,
|
||||||
)
|
)
|
||||||
|
|
@ -103,6 +109,7 @@ class ElementColors(
|
||||||
gray400 = other.gray400
|
gray400 = other.gray400
|
||||||
gray1400 = other.gray1400
|
gray1400 = other.gray1400
|
||||||
textActionCritical = other.textActionCritical
|
textActionCritical = other.textActionCritical
|
||||||
|
accentColor = other.accentColor
|
||||||
placeholder = other.placeholder
|
placeholder = other.placeholder
|
||||||
isLight = other.isLight
|
isLight = other.isLight
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
@Composable
|
@Composable
|
||||||
fun Divider(
|
fun Divider(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
thickness: Dp = DividerDefaults.Thickness,
|
thickness: Dp = ElementDividerDefaults.thickness,
|
||||||
color: Color = DividerDefaults.color,
|
color: Color = DividerDefaults.color,
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.Divider(
|
androidx.compose.material3.Divider(
|
||||||
|
|
@ -42,6 +42,10 @@ fun Divider(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object ElementDividerDefaults {
|
||||||
|
val thickness = 0.5.dp
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.Dividers)
|
@Preview(group = PreviewGroup.Dividers)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DividerPreview() = ElementThemedPreview {
|
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.onPreviewKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
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)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
fun Modifier.onTabOrEnterKeyFocusNext(focusManager: FocusManager): Modifier = onPreviewKeyEvent { event ->
|
fun Modifier.onTabOrEnterKeyFocusNext(focusManager: FocusManager): Modifier = onPreviewKeyEvent { event ->
|
||||||
if (event.key == Key.Tab || event.key == Key.Enter) {
|
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.LocalAutofill
|
||||||
import androidx.compose.ui.platform.LocalAutofillTree
|
import androidx.compose.ui.platform.LocalAutofillTree
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
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)
|
@Preview(group = PreviewGroup.TextFields)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TextFieldLightPreview() =
|
internal fun TextFieldLightPreview() =
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,8 @@ dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
implementation(platform(libs.network.okhttp.bom))
|
implementation(platform(libs.network.okhttp.bom))
|
||||||
implementation("com.squareup.okhttp3:okhttp")
|
implementation(libs.network.okhttp)
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
implementation(libs.network.okhttp.logging)
|
||||||
|
|
||||||
implementation(libs.network.retrofit)
|
implementation(libs.network.retrofit)
|
||||||
implementation(libs.network.retrofit.converter.serialization)
|
implementation(libs.network.retrofit.converter.serialization)
|
||||||
implementation(libs.serialization.json)
|
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.AppScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger
|
import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Protocol
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
object NetworkModule {
|
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
|
@Provides
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
fun providesOkHttpClient(
|
fun providesOkHttpClient(
|
||||||
httpLoggingInterceptor: HttpLoggingInterceptor,
|
buildMeta: BuildMeta,
|
||||||
): OkHttpClient {
|
): OkHttpClient = OkHttpClient.Builder().apply {
|
||||||
return OkHttpClient.Builder()
|
connectTimeout(30, TimeUnit.SECONDS)
|
||||||
// workaround for #4669
|
readTimeout(60, TimeUnit.SECONDS)
|
||||||
.protocols(listOf(Protocol.HTTP_1_1))
|
writeTimeout(60, TimeUnit.SECONDS)
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
if (buildMeta.isDebuggable) addInterceptor(providesHttpLoggingInterceptor())
|
||||||
.readTimeout(60, TimeUnit.SECONDS)
|
}.build()
|
||||||
.writeTimeout(60, TimeUnit.SECONDS)
|
|
||||||
.addInterceptor(httpLoggingInterceptor)
|
@Provides
|
||||||
.build()
|
@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
|
package io.element.android.libraries.network
|
||||||
|
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
import dagger.Lazy
|
|
||||||
import io.element.android.libraries.core.uri.ensureTrailingSlash
|
import io.element.android.libraries.core.uri.ensureTrailingSlash
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
class RetrofitFactory @Inject constructor(
|
class RetrofitFactory @Inject constructor(
|
||||||
private val okHttpClient: Lazy<OkHttpClient>,
|
private val okHttpClient: Provider<OkHttpClient>,
|
||||||
|
private val json: Provider<Json>,
|
||||||
) {
|
) {
|
||||||
fun create(baseUrl: String): Retrofit {
|
fun create(baseUrl: String): Retrofit = Retrofit.Builder()
|
||||||
val contentType = "application/json".toMediaType()
|
.baseUrl(baseUrl.ensureTrailingSlash())
|
||||||
return Retrofit.Builder()
|
.addConverterFactory(json.get().asConverterFactory("application/json".toMediaType()))
|
||||||
.baseUrl(baseUrl.ensureTrailingSlash())
|
.callFactory(okHttpClient.get())
|
||||||
.addConverterFactory(Json.asConverterFactory(contentType))
|
.build()
|
||||||
.callFactory { request -> okHttpClient.get().newCall(request) }
|
|
||||||
.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
|
package io.element.android.libraries.textcomposer
|
||||||
|
|
||||||
import android.graphics.Color
|
import androidx.compose.foundation.Image
|
||||||
import android.net.Uri
|
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.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.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.MaterialTheme
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.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.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import io.element.android.libraries.designsystem.VectorIcons
|
||||||
import androidx.core.view.isInvisible
|
import io.element.android.libraries.designsystem.modifiers.applyIf
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
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.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TextComposer(
|
fun TextComposer(
|
||||||
fullscreen: Boolean,
|
|
||||||
composerText: String?,
|
composerText: String?,
|
||||||
composerMode: MessageComposerMode,
|
composerMode: MessageComposerMode,
|
||||||
composerCanSendMessage: Boolean,
|
composerCanSendMessage: Boolean,
|
||||||
isInDarkMode: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
focusRequester: FocusRequester = FocusRequester(),
|
||||||
onSendMessage: (String) -> Unit = {},
|
onSendMessage: (String) -> Unit = {},
|
||||||
onFullscreenToggle: () -> Unit = {},
|
onResetComposerMode: () -> Unit = {},
|
||||||
onCloseSpecialMode: () -> Unit = {},
|
|
||||||
onComposerTextChange: (CharSequence) -> Unit = {},
|
onComposerTextChange: (CharSequence) -> Unit = {},
|
||||||
onAddAttachment:() -> Unit = {},
|
onAddAttachment:() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
if (LocalInspectionMode.current) {
|
val text = composerText.orEmpty()
|
||||||
FakeComposer(modifier)
|
Row(modifier.padding(
|
||||||
} else {
|
horizontal = 12.dp,
|
||||||
val focusRequester = FocusRequester()
|
vertical = 8.dp
|
||||||
AndroidView(
|
), verticalAlignment = Alignment.Bottom) {
|
||||||
modifier = modifier.focusRequester(focusRequester),
|
AttachmentButton(onClick = onAddAttachment, modifier = Modifier.padding(vertical = 6.dp))
|
||||||
factory = { context ->
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
RichTextComposerLayout(context).apply {
|
var lineCount by remember { mutableStateOf(0) }
|
||||||
// Sets up listeners for View -> Compose communication
|
val roundedCorners = remember(lineCount, composerMode) {
|
||||||
this.callback = object : Callback {
|
if (lineCount > 1 || composerMode is MessageComposerMode.Special) {
|
||||||
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
RoundedCornerShape(20.dp)
|
||||||
return false
|
} else {
|
||||||
}
|
RoundedCornerShape(28.dp)
|
||||||
|
|
||||||
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 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
|
@Composable
|
||||||
private fun FakeComposer(
|
private fun ComposerModeView(
|
||||||
|
composerMode: MessageComposerMode,
|
||||||
|
onResetComposerMode: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
// AndroidView is not Available in this mode, just render a Text
|
when (composerMode) {
|
||||||
Box(
|
is MessageComposerMode.Edit -> {
|
||||||
modifier = modifier
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
.fillMaxWidth()
|
modifier = modifier
|
||||||
.height(80.dp)
|
.fillMaxWidth()
|
||||||
) {
|
.padding(horizontal = 12.dp, vertical = 8.dp)) {
|
||||||
Text(
|
Icon(
|
||||||
modifier = Modifier
|
resourceId = VectorIcons.Edit,
|
||||||
.align(Alignment.Center),
|
contentDescription = stringResource(R.string.editing),
|
||||||
textAlign = TextAlign.Center,
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
text = "Composer Preview",
|
modifier = Modifier.size(16.dp),
|
||||||
fontSize = 20.sp,
|
)
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
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) {
|
@Composable
|
||||||
val editTextColor = if (isDarkMode) {
|
private fun AttachmentButton(
|
||||||
Color.WHITE
|
onClick: () -> Unit,
|
||||||
} else {
|
modifier: Modifier = Modifier
|
||||||
Color.BLACK
|
) {
|
||||||
|
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
|
@Preview
|
||||||
|
|
@ -162,15 +297,38 @@ internal fun TextComposerDarkPreview() = ElementPreviewDark { ContentToPreview()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview() {
|
private fun ContentToPreview() {
|
||||||
TextComposer(
|
Column {
|
||||||
onSendMessage = {},
|
TextComposer(
|
||||||
fullscreen = false,
|
onSendMessage = {},
|
||||||
onFullscreenToggle = { },
|
onComposerTextChange = {},
|
||||||
onComposerTextChange = {},
|
composerMode = MessageComposerMode.Normal(""),
|
||||||
composerMode = MessageComposerMode.Normal(""),
|
onResetComposerMode = {},
|
||||||
onCloseSpecialMode = {},
|
composerCanSendMessage = false,
|
||||||
composerCanSendMessage = true,
|
composerText = "",
|
||||||
composerText = "Message",
|
)
|
||||||
isInDarkMode = true,
|
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:mediapickers:impl"))
|
||||||
implementation(project(":libraries:mediaupload:impl"))
|
implementation(project(":libraries:mediaupload:impl"))
|
||||||
implementation(project(":libraries:usersearch:impl"))
|
implementation(project(":libraries:usersearch:impl"))
|
||||||
|
implementation(project(":libraries:textcomposer"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DependencyHandlerScope.allServicesImpl() {
|
fun DependencyHandlerScope.allServicesImpl() {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d941ac36664583640991d6a310edf45b4f87d8285647d6aa661970dba3c365b4
|
oid sha256:8296d9d11de5cc99531910ca7252377e084234b09dfb49e1f5b9fa2d1ce602cf
|
||||||
size 83119
|
size 83194
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:99f4447da79ca0c37c7607fb673b6dfd29cb241ba30420adcc0399db170a5810
|
oid sha256:5c0668727ef819f0f5d2107ce3b6210110879d616c7aeda7d87e54fee1ec6f89
|
||||||
size 98150
|
size 99242
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:12839404b6e9eaf8983ccf3e5773cd8c80174068e76924ad72b7b6429018b0b6
|
oid sha256:e556931f594409bbf654dd410a25e418935bb8ffd5b7a8eea429a69b0c05424c
|
||||||
size 79478
|
size 79592
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4115bdacc08b7c5bdb4c2859d131eaa0e84748d3f42e2de388ffef72131851a2
|
oid sha256:048e556d97ea6301fdb6991776fd4f089e56e227d4babb60c73f350ed97b45a9
|
||||||
size 93105
|
size 94026
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0d7a3255202968ab6a467b698ca41154b4564882ac3e45578ea36060a8c97583
|
oid sha256:55d18860fbd298703c4f2345fea13bb6edaa065727aed4f043d6ed571a5d091c
|
||||||
size 61586
|
size 61509
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0bab25b90e451021883fcd388030ac054729d1ea28a2b4e9b6041f5a103f0a7b
|
oid sha256:68b9ca77722425893f3f28ab5df3770409b5b3a56b64eeae08e7c82d9a1e3e80
|
||||||
size 99914
|
size 99857
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1a3a36f97c7c22d4113570ce8413de29c1d62e96d1d979851a66f051ca229e08
|
oid sha256:e3b2315704405e618e19f6d3e8d6d19ce0564e621b81a6a78e157ac1b2a0487b
|
||||||
size 59148
|
size 59131
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b28cfcb8e238559bd7775507397307fef4e192bb9e1615a0b08f048e73a5e6dd
|
oid sha256:808627e2b8e737b673a03a505e671a96e58ead5b9ccf896ad71e802346dac638
|
||||||
size 96054
|
size 96033
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:88f8d524d6a22a05b013c8cdc73b909a4ca41938d0fdb41923381c2045e6b37f
|
oid sha256:edfafd24c8085ba7fe218c720b9dc2fead18d729b6ddea3b9fc04b51cb7c0e91
|
||||||
size 8990
|
size 9418
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f6ff5a856c3af2b85848a80e7d832ebbf737e7d5cf06105d5a01c1e6b3f1eeb7
|
oid sha256:f6b92a6d4f1a2287cefe06c6ba7cb2a2210d61cf4c38747ffd308e4455d78f71
|
||||||
size 9115
|
size 9335
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7e20ba9c18742b91164f5d0bdbbfe41b3960ab67bf921c2d305cfd72fbe57e52
|
oid sha256:596e4e45b76ab154f3b4011a1ef3fbd27ae801a758949c7a901262c752e7eb6e
|
||||||
size 33100
|
size 33461
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3dfa909234ae27da447b12391d53657afd23ef0dd17cff4c8bb55ef1b9811432
|
oid sha256:46cb0ed395274b4201da2bc604695e2cd805ad74cff58cc1ba341d38cb63dd6f
|
||||||
size 38347
|
size 38863
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7c1b9dc41e7f4abce90208ce21b2537d8522e1467afdc702735f2d3ab79ed265
|
oid sha256:84aae5ef0c7c9d8308e56b290cdbab809e6e732181fbcbcb22be794d8cb2f34b
|
||||||
size 38320
|
size 38780
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d9f210f49c4b9a17b01fffe23229afd6dc7909a50ff79732c4d7f169f6c6c558
|
oid sha256:0d59f1042dfdeb97fb2992c21c2c50a6ae1f8535a5b72063c874a91c1ece6076
|
||||||
size 18098
|
size 18355
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4782d54f26127fa3e8eb2674486c093036310febc29fc883731eddab158cad40
|
oid sha256:26b409cd4f3c950458e12b1ae3dcd586705edf6f2d79a549aab8b84b59578314
|
||||||
size 33261
|
size 33578
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d0741f10b79b3ba739a7d2f160e61df8ea30c7e90d5b03db66cf0fadae39637d
|
oid sha256:c4a7984c30e35b3b1641002b5facfe08dfab34e18c12383f82ae479c5ab57e25
|
||||||
size 31877
|
size 32435
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f9e818b80358ed35dfe0b1b1d26b8e64a137aa6f2fbce02385cb3cc7900bd852
|
oid sha256:ff299a00cc2b4006fae62e24765a2810fcca23cf56e4b8df86b09e8588a96dc4
|
||||||
size 37293
|
size 37631
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:18dd6a307f1990b95cddba7e5ae199fc2c9746fdc49231ceb320558707c1ec1c
|
oid sha256:9942eefb85c7525fe17184efa0b5dae53d7b1d76eff618b7ff99e90ae7220893
|
||||||
size 37252
|
size 37632
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7c0e24c4fc71151ae8dc04a6213ecc15e0d25dbe6b1684d177086572aeff84c5
|
oid sha256:074e0273e41666a37e1c2d0887956a9949d84364518e89ae31cb2523f1c3b37d
|
||||||
size 17289
|
size 17417
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:49048bedc855a3460f7a3e7c387481c53e4633ef4110f120c5f8ce649980a583
|
oid sha256:721007e228a1fac6c5e61f6a17ae5d2871fe6515a3f351a4ec91214a5b4e4538
|
||||||
size 32034
|
size 32602
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4bddb1e4c74494ed89812a31e35c8d056a3109043fa817c1dbae13d522e56566
|
oid sha256:8340785a1a871d93d9acda9d788d63c91b8270c747bb7b7748b7e532f56ed7e5
|
||||||
size 66641
|
size 66547
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4d6e5e784a6ee50cbf767bc0da43b493e910ee2a7ace9890419916d23c1e11a3
|
oid sha256:b3b668c53fbb814eb7c6c3a37f068e9475e0dd286a1099581e17c41b806cfa31
|
||||||
size 45461
|
size 48090
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f74cb8516eaf4d8f0e289f685d520bc130df21cbfc4fbc68eed001c4138c83b2
|
oid sha256:43f0d1fbafcde1e07b8ea58a38505916d7aa987aac8d9f3aef323f9001a80344
|
||||||
size 45907
|
size 48525
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:360c2c69a3d03538f6f61847757a15e88051b38195cadcd33f9cf2e57638b3f1
|
oid sha256:8662ededaa1c6682d2571e3398d6007dcf92a392efc59985c64bd48d68e2c296
|
||||||
size 41761
|
size 43088
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:360c2c69a3d03538f6f61847757a15e88051b38195cadcd33f9cf2e57638b3f1
|
oid sha256:8662ededaa1c6682d2571e3398d6007dcf92a392efc59985c64bd48d68e2c296
|
||||||
size 41761
|
size 43088
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:75f493a62614704c956b8be9351532bb431cc69e400adc9f1d536579b6dc731c
|
oid sha256:66a2c37dbb7478d45546d8ae404befe0ed0efa7c3ff467566fc4b4b9b76b7628
|
||||||
size 64226
|
size 64363
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e96dae6cc767476bd60f23079776600449414a9423976b85c91c348ed065d7ae
|
oid sha256:8219ba85624c3bdc6671b2135aa04d16597fd4087ed38456769c161471984012
|
||||||
size 44877
|
size 47443
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fd0bb44da30359d50cf5754768d58d75bf4489c9d6fe9c4d5c0d2f3fd9020541
|
oid sha256:6c0dfdc6684fece8358de955497762f43affd44fc44d35ce8982b2e713b0006f
|
||||||
size 45302
|
size 47873
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9654f73bd95d1b0d03dc6ea44d13c4910fb8e64a3c86644a11d5bc8591a4be8a
|
oid sha256:352cce29147cc831da72f7d0a5948d3d0841e05c168f86a09492fa6ab44942e9
|
||||||
size 41312
|
size 42405
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9654f73bd95d1b0d03dc6ea44d13c4910fb8e64a3c86644a11d5bc8591a4be8a
|
oid sha256:352cce29147cc831da72f7d0a5948d3d0841e05c168f86a09492fa6ab44942e9
|
||||||
size 41312
|
size 42405
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ee403297e15204e165ec5dce90e3e93e95e8e1b2e2cbd03489b91c4c9a6de083
|
oid sha256:34d59f799c43ffcaa65b5af54e35a7507376db666093c4d5698d4125bd1e2228
|
||||||
size 10360
|
size 10796
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:33237fb8f262176e6f291a8ded90ab56600113c93bed188f6d4d01df950df53e
|
oid sha256:36b3bc2c243a8c09bbdba33f617a6d0fdbb0f5943560e0f23008cc129d1b5682
|
||||||
size 9286
|
size 9520
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:22afae6cd16ea0134b0966e5fbd7003592b9133e3d57947f062b7c9ebd4694d4
|
oid sha256:14286b8d22070f428630ae1d4244da42f33d4aa4906fbcc09cae4a034ded7707
|
||||||
size 33717
|
size 33693
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:68e175fb1c070b17a2f7ef6644837fb37b906b122df737bb9740aa591aaddd34
|
oid sha256:e30d0f0aff589c97e152b48d701a66ff323ab74aa1215af8dc1962bbff1a7204
|
||||||
size 32943
|
size 32867
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a48ccf1ff3570915b0673de7c0367d951b795c833d7896645b37000b8dc09c15
|
oid sha256:8c88bfa85e91e6ab247e3feec199c8a29233827dbd2aad28cf9ff00ea9b99117
|
||||||
size 9649
|
size 10927
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:945b360a62877d16ec7c25942158ae47e244df85118ca861482ff3bbf0aef55f
|
oid sha256:36adfe10149d33ce817d520d3f542b5441713f7ceddc5864747f624f5e26d432
|
||||||
size 9268
|
size 10620
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ce6cbcf47c5e76d72fd9bf15e5676510ac29a3087cc862c67ccd4f7de1f24359
|
oid sha256:7fbb93faeae669493b42bc5831b1c5bb67b25ed688d582e01249a9e23e03425a
|
||||||
size 45546
|
size 47340
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9af27bab7bbac9914f4e2694f1ea9c200c57edb345bd107cfb5c2edb48cd056e
|
oid sha256:e5e5ac66335825c321666fe9564d1dbbd00ebbfe67f39d89ac133fcbd2b36b70
|
||||||
size 45725
|
size 47473
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ce6cbcf47c5e76d72fd9bf15e5676510ac29a3087cc862c67ccd4f7de1f24359
|
oid sha256:72bc2d1171ca0d80bfc4522bbbb15c5973dbae362351c344f9b25382da4831d0
|
||||||
size 45546
|
size 47701
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:99e4c0fe8076952a490ab58807913984a9a6598b589c5e35bc9eececc741bf11
|
oid sha256:603a9bc343feceb5c1f86a36dcfdca7edf71a150237419a16895d12247b032d2
|
||||||
size 44666
|
size 46271
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:46660a369ee182afbf49c5d8022fa79c4a358aee7e2aea83bdf08f66a16b5ff1
|
oid sha256:b06f5cdf95f2850a488e71c24c9857eca7b62d147b46b77b3c2ffece75789491
|
||||||
size 45095
|
size 46682
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:99e4c0fe8076952a490ab58807913984a9a6598b589c5e35bc9eececc741bf11
|
oid sha256:b4a13a0332f1d5ce24824434cb909de2d2bf4af601e6924155d2a550d9e03ec0
|
||||||
size 44666
|
size 46634
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:18569af3c955f9280122faf1ac653af2f569ee54bfa6d6d74b83adb2813ee04e
|
oid sha256:036d832c19fcba93b8848f9687f4029eba0beeb56893b740062b18c769f419cd
|
||||||
size 35446
|
size 35424
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:196cff5b2ee404d5547803039a919f565058a9a0b4c586b05458b63b37b54fd5
|
oid sha256:22eb3320d95bb28b621ff2e7b3421f79b539d84a12591c885f088b3ae404b209
|
||||||
size 28153
|
size 28130
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:05fc7b6fa4e71ae48e795422184bcefd809c568f62a0709d08ffca045f078e66
|
oid sha256:55ae80019f919603ec36672b0cc1f0571a9b9135994121fcc2b3420ffc667c40
|
||||||
size 55574
|
size 55567
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:119ae9d67a9614e8b461d3a84dac8a59dfa59a72bdee5e178fe317cf2717f5d6
|
oid sha256:b423c2a214c752e980b14181df543f178bf3aa878b0999802cee35d0a79cfb73
|
||||||
size 34823
|
size 34801
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:569a281b9ceeadebbf84572aa6f124ccacf8e82d88102b7e7af627350c252afb
|
oid sha256:a02bb25e5802305dcd08aa092f7201fb6e1e59c6e8dc8b6760756b75786de82f
|
||||||
size 34453
|
size 34433
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f659dc9210a8165e992efa8b2884535417be7e4b35e83b1fc44b76fab9eeb2a7
|
oid sha256:4ced1625c98be58c1fe1c4f29dc2e4df78abfabda0276274616f09dfefa65f8f
|
||||||
size 29956
|
size 30517
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e3c1ac7bb831e3fc9d9ac8500849342095a4a35dff2ee12879051e9bffb73894
|
oid sha256:16db29fe1b26c978613cacadc059bd8d01796785d96444142cd14415154ea1d2
|
||||||
size 35541
|
size 35521
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4603362c3c2dfa9e002eb64c14eb0969ff749a4f0315e2b8810a58253ae42d21
|
oid sha256:dac8c13ec5b3086829d50d92efc8cece7e60ffcb475cc01c0a8ccfc3069ae4e7
|
||||||
size 27992
|
size 27973
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f199a3afe2771b33953e56af4f30f6ece7bd8ae421efd3b5601f6393c2d07617
|
oid sha256:fbf110969859056024143b3665ddf735e0c772b4706890db194071107910199e
|
||||||
size 56336
|
size 56316
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:72847de27dbb5a0126b90c388b14ef443838c5832e8a37f1d29f51b2eb404d97
|
oid sha256:9e45a51563799bd8ca32d56154819cd8df0fa6a466ae48ed809e3e40a2acd3b1
|
||||||
size 34086
|
size 34064
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e227432ad7515e2f060ed665861d65f502c354e700fcc428faa6ef20717c56de
|
oid sha256:45c113cdecc92a2210df1bc595159b0a02a115d527bf5f9344b1e177147d6770
|
||||||
size 34100
|
size 34076
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7df2eb3bc330ca400199bcf233914b91bd37e83ea6fd6bc9ac2dfe81102b85ed
|
oid sha256:10bcb62743748e2e04ceaf5e82a70ecf42f7361fa88ce0d7d63079ee45ce5209
|
||||||
size 30137
|
size 30155
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6ce332185060fe0571610e9a060ce6dfafc7619dad384725456a4a755c39a123
|
oid sha256:9e744f45c0a29e3cdc6d0d5417b9702b1a580c557e6d280e046b4a29319bb11f
|
||||||
size 61131
|
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