[Message actions] New UI for message composer and editing (#526)

* Add UI for edit composer mode

* Remove leftover code from the RTE implementation

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2023-06-07 13:06:19 +02:00 committed by GitHub
parent ff25a1a057
commit e376c1be82
29 changed files with 425 additions and 877 deletions

View file

@ -5,8 +5,6 @@ appId: ${APP_ID}
- inputText: ${ROOM_NAME.substring(0, 3)}
- takeScreenshot: build/maestro/400-SearchRoom
- tapOn: ${ROOM_NAME}
# Close keyboard
- hideKeyboard
# Back from timeline
- back
# Close keyboard

View file

@ -4,10 +4,8 @@ appId: ${APP_ID}
# TODO Create a room on a new account
- tapOn: ${ROOM_NAME}
- takeScreenshot: build/maestro/500-Timeline
- tapOn: "Message"
- tapOn: "Message"
- inputText: "Hello world!"
- tapOn: "Toggle full screen mode"
- tapOn: "Toggle full screen mode"
- tapOn: "Send"
- hideKeyboard
- back

1
changelog.d/484.feature Normal file
View file

@ -0,0 +1 @@
New UI for composer and editing messages

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.textcomposer.TextComposer
@Composable
@ -52,17 +51,14 @@ fun MessageComposerView(
TextComposer(
onSendMessage = ::sendMessage,
fullscreen = state.isFullScreen,
onFullscreenToggle = ::onFullscreenToggle,
composerMode = state.mode,
onCloseSpecialMode = ::onCloseSpecialMode,
onResetComposerMode = ::onCloseSpecialMode,
onComposerTextChange = ::onComposerTextChange,
onAddAttachment = {
state.eventSink(MessageComposerEvents.AddAttachment)
},
composerCanSendMessage = state.isSendButtonVisible,
composerText = state.text?.charSequence?.toString(),
isInDarkMode = !ElementTheme.colors.isLight,
modifier = modifier
)
}

View file

@ -46,6 +46,7 @@ fun elementColorsDark() = ElementColors(
gray400 = Compound_Gray_400_Dark,
gray1400 = Compound_Gray_1400_Dark,
textActionCritical = TextColorCriticalDark,
accentColor = Color(0xFF0DBD8B),
placeholder = Compound_Gray_800_Dark,
isLight = false,
)

View file

@ -46,6 +46,7 @@ fun elementColorsLight() = ElementColors(
gray400 = Compound_Gray_400_Light,
gray1400 = Compound_Gray_1400_Light,
textActionCritical = TextColorCriticalLight,
accentColor = Color(0xFF0DBD8B),
placeholder = Compound_Gray_800_Light,
isLight = true,
)

View file

@ -33,6 +33,7 @@ class ElementColors(
gray400: Color,
gray1400: Color,
textActionCritical: Color,
accentColor: Color,
placeholder: Color,
isLight: Boolean
) {
@ -61,6 +62,9 @@ class ElementColors(
var textActionCritical by mutableStateOf(textActionCritical)
private set
var accentColor by mutableStateOf(accentColor)
private set
var placeholder by mutableStateOf(placeholder)
private set
@ -77,6 +81,7 @@ class ElementColors(
gray400: Color = this.gray400,
gray1400: Color = this.gray1400,
textActionCritical: Color = this.textActionCritical,
accentColor: Color = this.accentColor,
placeholder: Color = this.placeholder,
isLight: Boolean = this.isLight,
) = ElementColors(
@ -89,6 +94,7 @@ class ElementColors(
gray400 = gray400,
gray1400 = gray1400,
textActionCritical = textActionCritical,
accentColor = accentColor,
placeholder = placeholder,
isLight = isLight,
)
@ -103,6 +109,7 @@ class ElementColors(
gray400 = other.gray400
gray1400 = other.gray1400
textActionCritical = other.textActionCritical
accentColor = other.accentColor
placeholder = other.placeholder
isLight = other.isLight
}

View file

@ -39,6 +39,7 @@ import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -95,6 +96,53 @@ fun OutlinedTextField(
)
}
@Composable
fun OutlinedTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = OutlinedTextFieldDefaults.shape,
colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
) {
androidx.compose.material3.OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
textStyle = textStyle,
label = label,
placeholder = placeholder,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
supportingText = supportingText,
isError = isError,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
interactionSource = interactionSource,
shape = shape,
colors = colors,
)
}
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.onTabOrEnterKeyFocusNext(focusManager: FocusManager): Modifier = onPreviewKeyEvent { event ->
if (event.key == Key.Tab || event.key == Key.Enter) {

View file

@ -40,6 +40,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalAutofill
import androidx.compose.ui.platform.LocalAutofillTree
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -96,6 +97,53 @@ fun TextField(
)
}
@Composable
fun TextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = TextFieldDefaults.shape,
colors: TextFieldColors = TextFieldDefaults.colors()
) {
androidx.compose.material3.TextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
textStyle = textStyle,
label = label,
placeholder = placeholder,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
supportingText = supportingText,
isError = isError,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
interactionSource = interactionSource,
shape = shape,
colors = colors,
)
}
@Preview(group = PreviewGroup.TextFields)
@Composable
internal fun TextFieldLightPreview() =

View file

@ -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()
}

View file

@ -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
}
}
}

View file

@ -16,140 +16,275 @@
package io.element.android.libraries.textcomposer
import android.graphics.Color
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.VectorIcons
import io.element.android.libraries.designsystem.modifiers.applyIf
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TextComposer(
fullscreen: Boolean,
composerText: String?,
composerMode: MessageComposerMode,
composerCanSendMessage: Boolean,
isInDarkMode: Boolean,
modifier: Modifier = Modifier,
focusRequester: FocusRequester = FocusRequester(),
onSendMessage: (String) -> Unit = {},
onFullscreenToggle: () -> Unit = {},
onCloseSpecialMode: () -> Unit = {},
onResetComposerMode: () -> Unit = {},
onComposerTextChange: (CharSequence) -> Unit = {},
onAddAttachment:() -> Unit = {},
) {
if (LocalInspectionMode.current) {
FakeComposer(modifier)
} else {
val focusRequester = FocusRequester()
AndroidView(
modifier = modifier.focusRequester(focusRequester),
factory = { context ->
RichTextComposerLayout(context).apply {
// Sets up listeners for View -> Compose communication
this.callback = object : Callback {
override fun onRichContentSelected(contentUri: Uri): Boolean {
return false
}
override fun onTextChanged(text: CharSequence) {
onComposerTextChange(text)
}
override fun onCloseRelatedMessage() {
onCloseSpecialMode()
}
override fun onSendMessage(text: CharSequence) {
// text contains markdown.
onSendMessage(text.toString())
}
override fun onAddAttachment() {
onAddAttachment()
}
override fun onExpandOrCompactChange() {
}
override fun onFullScreenModeChanged() {
onFullscreenToggle()
}
}
setFullScreen(fullscreen, animated = false, manageKeyboard = true)
(this as MessageComposerView).apply {
setup(isInDarkMode, composerMode)
}
}
},
update = { view ->
// View's been inflated or state read in this block has been updated
// Add logic here if necessary
// As selectedItem is read here, AndroidView will recompose
// whenever the state changes
// Example of Compose -> View communication
val messageComposerView = (view as MessageComposerView)
view.setFullScreen(fullscreen, animated = false, manageKeyboard = false)
messageComposerView.renderComposerMode(composerMode)
messageComposerView.sendButton.isInvisible = !composerCanSendMessage
messageComposerView.setTextIfDifferent(composerText ?: "")
messageComposerView.editText.requestFocus()
val text = composerText.orEmpty()
Row(modifier.padding(
horizontal = 12.dp,
vertical = 8.dp
), verticalAlignment = Alignment.Bottom) {
AttachmentButton(onClick = onAddAttachment, modifier = Modifier.padding(vertical = 6.dp))
Spacer(modifier = Modifier.width(12.dp))
var lineCount by remember { mutableStateOf(0) }
val roundedCorners = remember(lineCount, composerMode) {
if (lineCount > 1 || composerMode is MessageComposerMode.Special) {
RoundedCornerShape(20.dp)
} else {
RoundedCornerShape(28.dp)
}
)
}
val minHeight = 42.dp
Column(
modifier = Modifier
.fillMaxWidth()
.clip(roundedCorners)
.background(MaterialTheme.colorScheme.surfaceVariant)
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, roundedCorners)
) {
if (composerMode is MessageComposerMode.Special) {
ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode)
}
val defaultTypography = ElementTextStyles.Regular.callout.copy(textAlign = TextAlign.Start)
Box {
BasicTextField(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = minHeight)
.focusRequester(focusRequester),
value = text,
onValueChange = { onComposerTextChange(it) },
onTextLayout = {
lineCount = it.lineCount
},
textStyle = defaultTypography.copy(color = MaterialTheme.colorScheme.primary),
cursorBrush = SolidColor(LocalColors.current.accentColor),
decorationBox = { innerTextField ->
TextFieldDefaults.DecorationBox(
value = text,
innerTextField = innerTextField,
enabled = true,
singleLine = false,
visualTransformation = VisualTransformation.None,
shape = roundedCorners,
contentPadding = PaddingValues(top = 10.dp, bottom = 10.dp, start = 12.dp, end = 42.dp),
interactionSource = remember { MutableInteractionSource() },
placeholder = {
Text(stringResource(StringR.string.common_message), style = defaultTypography)
},
colors = TextFieldDefaults.colors(
unfocusedTextColor = MaterialTheme.colorScheme.secondary,
focusedTextColor = MaterialTheme.colorScheme.primary,
unfocusedPlaceholderColor = MaterialTheme.colorScheme.secondary,
focusedPlaceholderColor = MaterialTheme.colorScheme.secondary,
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
errorContainerColor = MaterialTheme.colorScheme.surfaceVariant,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
)
)
}
)
SendButton(
text = text,
canSendMessage = composerCanSendMessage,
onSendMessage = onSendMessage,
composerMode = composerMode,
modifier = Modifier.padding(end = 6.dp, bottom = 6.dp)
)
}
}
}
}
@Composable
private fun FakeComposer(
private fun ComposerModeView(
composerMode: MessageComposerMode,
onResetComposerMode: () -> Unit,
modifier: Modifier = Modifier,
) {
// AndroidView is not Available in this mode, just render a Text
Box(
modifier = modifier
.fillMaxWidth()
.height(80.dp)
) {
Text(
modifier = Modifier
.align(Alignment.Center),
textAlign = TextAlign.Center,
text = "Composer Preview",
fontSize = 20.sp,
color = MaterialTheme.colorScheme.secondary,
)
when (composerMode) {
is MessageComposerMode.Edit -> {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp)) {
Icon(
resourceId = VectorIcons.Edit,
contentDescription = stringResource(R.string.editing),
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier.size(16.dp),
)
Text(
stringResource(R.string.editing),
style = ElementTextStyles.Regular.caption2,
textAlign = TextAlign.Start,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(StringR.string.action_close),
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.size(16.dp)
.clickable(
enabled = true,
onClick = onResetComposerMode,
interactionSource = MutableInteractionSource(),
indication = rememberRipple(bounded = false)
),
)
}
}
else -> Unit
}
}
private fun MessageComposerView.setup(isDarkMode: Boolean, composerMode: MessageComposerMode) {
val editTextColor = if (isDarkMode) {
Color.WHITE
} else {
Color.BLACK
@Composable
private fun AttachmentButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier) {
Surface(
Modifier
.size(30.dp)
.clickable(true, onClick = onClick),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary
) {
Image(
modifier = Modifier.size(12.5f.dp),
painter = painterResource(R.drawable.ic_add_attachment),
contentDescription = null,
contentScale = ContentScale.Inside,
colorFilter = ColorFilter.tint(
LocalContentColor.current
)
)
}
}
}
@Composable
private fun BoxScope.SendButton(
text: String,
canSendMessage: Boolean,
onSendMessage: (String) -> Unit,
composerMode: MessageComposerMode,
modifier: Modifier = Modifier,
) {
val interactionSource = MutableInteractionSource()
Box(
modifier = modifier
.clip(CircleShape)
.background(if (canSendMessage) LocalColors.current.accentColor else Color.Transparent)
.size(30.dp)
.align(Alignment.BottomEnd)
.applyIf(composerMode !is MessageComposerMode.Edit, ifTrue = {
padding(start = 1.dp) // Center the arrow in the circle
})
.clickable(
enabled = canSendMessage,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false),
onClick = {
onSendMessage(text)
}),
contentAlignment = Alignment.Center,
) {
val iconId = when (composerMode) {
is MessageComposerMode.Edit -> R.drawable.ic_tick
else -> R.drawable.ic_send
}
val contentDescription = when (composerMode) {
is MessageComposerMode.Edit -> stringResource(StringR.string.action_edit)
else -> stringResource(StringR.string.action_send)
}
Icon(
modifier = Modifier.size(16.dp),
resourceId = iconId,
contentDescription = contentDescription,
tint = if (canSendMessage) Color.White else LocalColors.current.quaternary
)
}
editText.setTextColor(editTextColor)
editText.setHintTextColor(editTextColor)
editText.setHint(R.string.rich_text_editor_composer_placeholder)
emojiButton?.isVisible = true
sendButton.isVisible = true
editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED
renderComposerMode(composerMode)
}
@Preview
@ -162,15 +297,38 @@ internal fun TextComposerDarkPreview() = ElementPreviewDark { ContentToPreview()
@Composable
private fun ContentToPreview() {
TextComposer(
onSendMessage = {},
fullscreen = false,
onFullscreenToggle = { },
onComposerTextChange = {},
composerMode = MessageComposerMode.Normal(""),
onCloseSpecialMode = {},
composerCanSendMessage = true,
composerText = "Message",
isInDarkMode = true,
)
Column {
TextComposer(
onSendMessage = {},
onComposerTextChange = {},
composerMode = MessageComposerMode.Normal(""),
onResetComposerMode = {},
composerCanSendMessage = false,
composerText = "",
)
TextComposer(
onSendMessage = {},
onComposerTextChange = {},
composerMode = MessageComposerMode.Normal(""),
onResetComposerMode = {},
composerCanSendMessage = true,
composerText = "A message",
)
TextComposer(
onSendMessage = {},
onComposerTextChange = {},
composerMode = MessageComposerMode.Normal(""),
onResetComposerMode = {},
composerCanSendMessage = true,
composerText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
)
TextComposer(
onSendMessage = {},
onComposerTextChange = {},
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text"),
onResetComposerMode = {},
composerCanSendMessage = true,
composerText = "A message",
)
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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>

View 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>

View 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>

View file

@ -98,6 +98,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
implementation(project(":libraries:mediapickers:impl"))
implementation(project(":libraries:mediaupload:impl"))
implementation(project(":libraries:usersearch:impl"))
implementation(project(":libraries:textcomposer"))
}
fun DependencyHandlerScope.allServicesImpl() {

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a48ccf1ff3570915b0673de7c0367d951b795c833d7896645b37000b8dc09c15
size 9649
oid sha256:8c88bfa85e91e6ab247e3feec199c8a29233827dbd2aad28cf9ff00ea9b99117
size 10927

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:945b360a62877d16ec7c25942158ae47e244df85118ca861482ff3bbf0aef55f
size 9268
oid sha256:36adfe10149d33ce817d520d3f542b5441713f7ceddc5864747f624f5e26d432
size 10620

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce6cbcf47c5e76d72fd9bf15e5676510ac29a3087cc862c67ccd4f7de1f24359
size 45546
oid sha256:7fbb93faeae669493b42bc5831b1c5bb67b25ed688d582e01249a9e23e03425a
size 47340

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9af27bab7bbac9914f4e2694f1ea9c200c57edb345bd107cfb5c2edb48cd056e
size 45725
oid sha256:e5e5ac66335825c321666fe9564d1dbbd00ebbfe67f39d89ac133fcbd2b36b70
size 47473

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce6cbcf47c5e76d72fd9bf15e5676510ac29a3087cc862c67ccd4f7de1f24359
size 45546
oid sha256:72bc2d1171ca0d80bfc4522bbbb15c5973dbae362351c344f9b25382da4831d0
size 47701

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:99e4c0fe8076952a490ab58807913984a9a6598b589c5e35bc9eececc741bf11
size 44666
oid sha256:603a9bc343feceb5c1f86a36dcfdca7edf71a150237419a16895d12247b032d2
size 46271

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46660a369ee182afbf49c5d8022fa79c4a358aee7e2aea83bdf08f66a16b5ff1
size 45095
oid sha256:b06f5cdf95f2850a488e71c24c9857eca7b62d147b46b77b3c2ffece75789491
size 46682

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:99e4c0fe8076952a490ab58807913984a9a6598b589c5e35bc9eececc741bf11
size 44666
oid sha256:b4a13a0332f1d5ce24824434cb909de2d2bf4af601e6924155d2a550d9e03ec0
size 46634

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7441f73e8567bbac360867f9b860621ec4766a67d5295d04cda45a09f942d0b5
size 47865

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f44fe7919578afe43e77b414f294e2ae7f1761c9528feeab548303428bfeba43
size 46117