diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9e65a56147..2332b3ebb2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ mavericks = "3.0.1" timber = "5.0.1" coil = "2.2.1" datetime = "0.4.0" -wysiwyg = "0.4.0" +wysiwyg = "0.7.0.1" serialization-json = "1.4.1" [libraries] diff --git a/libraries/core/src/main/java/io/element/android/x/core/ui/DimensionConverter.kt b/libraries/core/src/main/java/io/element/android/x/core/ui/DimensionConverter.kt new file mode 100644 index 0000000000..7564f77f14 --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/ui/DimensionConverter.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 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.x.core.ui + +import android.content.res.Resources +import android.util.TypedValue +import androidx.annotation.Px + +class DimensionConverter(val resources: Resources) { + + @Px + fun dpToPx(dp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + resources.displayMetrics + ).toInt() + } + + @Px + fun spToPx(sp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + sp.toFloat(), + resources.displayMetrics + ).toInt() + } + + fun pxToDp(@Px px: Int): Int { + return (px.toFloat() / resources.displayMetrics.density).toInt() + } +} diff --git a/libraries/core/src/main/java/io/element/android/x/core/ui/View.kt b/libraries/core/src/main/java/io/element/android/x/core/ui/View.kt new file mode 100644 index 0000000000..e9c0542c09 --- /dev/null +++ b/libraries/core/src/main/java/io/element/android/x/core/ui/View.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 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.x.core.ui + +import android.view.View +import android.view.inputmethod.InputMethodManager +import androidx.core.content.getSystemService + +fun View.hideKeyboard() { + val imm = context?.getSystemService() + imm?.hideSoftInputFromWindow(windowToken, 0) +} + +fun View.showKeyboard(andRequestFocus: Boolean = false) { + if (andRequestFocus) { + requestFocus() + } + val imm = context?.getSystemService() + imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) +} + +fun View.setHorizontalPadding(padding: Int) { + setPadding( + padding, + paddingTop, + padding, + paddingBottom + ) +} diff --git a/libraries/elementresources/src/main/res/drawable/bg_composer_rich_edit_text_expanded.xml b/libraries/elementresources/src/main/res/drawable/bg_composer_rich_edit_text_expanded.xml deleted file mode 100644 index 26d997e7db..0000000000 --- a/libraries/elementresources/src/main/res/drawable/bg_composer_rich_edit_text_expanded.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/libraries/elementresources/src/main/res/drawable/bg_composer_rich_edit_text_single_line.xml b/libraries/elementresources/src/main/res/drawable/bg_composer_rich_edit_text_single_line.xml deleted file mode 100644 index 7e2745a137..0000000000 --- a/libraries/elementresources/src/main/res/drawable/bg_composer_rich_edit_text_single_line.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/libraries/elementresources/src/main/res/values/dimens.xml b/libraries/elementresources/src/main/res/values/dimens.xml index 22c2a3e62c..1450693f76 100644 --- a/libraries/elementresources/src/main/res/values/dimens.xml +++ b/libraries/elementresources/src/main/res/values/dimens.xml @@ -43,13 +43,6 @@ 48dp 34dp - - 56dp - 52dp - 1dp - 28dp - 14dp - 28dp 6dp 350sp diff --git a/libraries/elementresources/src/main/res/values/styles_edit_text.xml b/libraries/elementresources/src/main/res/values/styles_edit_text.xml index b640fc49d9..94f4d86160 100644 --- a/libraries/elementresources/src/main/res/values/styles_edit_text.xml +++ b/libraries/elementresources/src/main/res/values/styles_edit_text.xml @@ -4,7 +4,7 @@ diff --git a/libraries/textcomposer/build.gradle.kts b/libraries/textcomposer/build.gradle.kts index f4f85f2420..7db9a252e5 100644 --- a/libraries/textcomposer/build.gradle.kts +++ b/libraries/textcomposer/build.gradle.kts @@ -11,6 +11,7 @@ android { dependencies { implementation(project(":libraries:elementresources")) + implementation(project(":libraries:core")) implementation(libs.wysiwyg) implementation(libs.androidx.constraintlayout) implementation("com.google.android.material:material:1.7.0") diff --git a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt new file mode 100644 index 0000000000..86e35d5ad0 --- /dev/null +++ b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerMode.kt @@ -0,0 +1,26 @@ +/* + * 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.x.textcomposer + +sealed interface MessageComposerMode { + data class Normal(val content: CharSequence?) : MessageComposerMode + + sealed class Special(open val event: Any /* TODO set correct type here */, open val defaultContent: CharSequence) : MessageComposerMode + data class Edit(override val event: Any, override val defaultContent: CharSequence) : Special(event, defaultContent) + class Quote(override val event: Any, override val defaultContent: CharSequence) : Special(event, defaultContent) + class Reply(override val event: Any, override val defaultContent: CharSequence) : Special(event, defaultContent) +} diff --git a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerView.kt b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerView.kt index 99f9b0aaa5..8592ec1809 100644 --- a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerView.kt +++ b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/MessageComposerView.kt @@ -20,36 +20,25 @@ import android.net.Uri import android.text.Editable import android.widget.EditText import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView // 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 - val fullScreenButton: ImageButton? - val composerRelatedMessageTitle: TextView - val composerRelatedMessageContent: TextView - val composerRelatedMessageImage: ImageView - val composerRelatedMessageActionIcon: ImageView - val composerRelatedMessageAvatar: ImageView var callback: Callback? - var isVisible: Boolean - - fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) - fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) fun setTextIfDifferent(text: CharSequence?): Boolean - fun replaceFormattedContent(text: CharSequence) - fun toggleFullScreen(newValue: Boolean) - - fun setInvisible(isInvisible: Boolean) + fun renderComposerMode(mode: MessageComposerMode) } interface Callback { @@ -65,4 +54,3 @@ interface Callback { fun onExpandOrCompactChange() fun onFullScreenModeChanged() } - diff --git a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/RichTextComposerLayout.kt b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/RichTextComposerLayout.kt index b96a8d9d2c..4906edd365 100644 --- a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/RichTextComposerLayout.kt +++ b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/RichTextComposerLayout.kt @@ -16,55 +16,70 @@ package io.element.android.x.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.ImageView -import android.widget.TextView +import android.widget.LinearLayout import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable +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.wysiwyg.EditorEditText import io.element.android.wysiwyg.inputhandlers.models.InlineFormat -import io.element.android.x.element.resources.R as ElementR +import io.element.android.x.core.ui.DimensionConverter +import io.element.android.x.core.ui.hideKeyboard +import io.element.android.x.core.ui.showKeyboard import io.element.android.x.textcomposer.databinding.ComposerRichTextLayoutBinding import io.element.android.x.textcomposer.databinding.ViewRichTextMenuButtonBinding -import io.element.android.x.textcomposer.tools.animateLayoutChange import io.element.android.x.textcomposer.tools.setTextIfDifferent +import uniffi.wysiwyg_composer.ActionState import uniffi.wysiwyg_composer.ComposerAction -import uniffi.wysiwyg_composer.MenuState +import io.element.android.x.element.resources.R as ElementR // Imported from Element Android class RichTextComposerLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView { +) : LinearLayout(context, attrs, defStyleAttr), MessageComposerView { private val views: ComposerRichTextLayoutBinding override var callback: Callback? = null - private var currentConstraintSetId: Int = -1 - private val animationDuration = 100L - private val maxEditTextLinesWhenCollapsed = 12 - - private val isFullScreen: Boolean get() = currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_fullscreen + // 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 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? @@ -83,39 +98,117 @@ class RichTextComposerLayout @JvmOverloads constructor( get() = views.sendButton override val attachmentButton: ImageButton get() = views.attachmentButton - override val fullScreenButton: ImageButton? - get() = views.composerFullScreenButton - override val composerRelatedMessageActionIcon: ImageView - get() = views.composerRelatedMessageActionIcon - override val composerRelatedMessageAvatar: ImageView - get() = views.composerRelatedMessageAvatar - override val composerRelatedMessageContent: TextView - get() = views.composerRelatedMessageContent - override val composerRelatedMessageImage: ImageView - get() = views.composerRelatedMessageImage - override val composerRelatedMessageTitle: TextView - get() = views.composerRelatedMessageTitle - override var isVisible: Boolean - get() = views.root.isVisible - set(value) { - views.root.isVisible = value + + // 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) { + editText.updateLayoutParams { + height = if (isFullScreen) 0 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 = isFullScreen + 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 = true + } init { inflate(context, R.layout.composer_rich_text_layout, this) views = ComposerRichTextLayoutBinding.bind(this) - collapse(false) + // 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() }) + TextChangeListener( + { callback?.onTextChanged(it) }, + { updateTextFieldBorder(isFullScreen) }) ) views.plainTextComposerEditText.addTextChangedListener( - TextChangeListener({ callback?.onTextChanged(it) }, { updateTextFieldBorder() }) + TextChangeListener( + { callback?.onTextChanged(it) }, + { updateTextFieldBorder(isFullScreen) }) ) - views.composerRelatedMessageCloseButton.setOnClickListener { - collapse() + disallowParentInterceptTouchEvent(views.richTextComposerEditText) + disallowParentInterceptTouchEvent(views.plainTextComposerEditText) + + views.composerModeCloseView.setOnClickListener { callback?.onCloseRelatedMessage() } @@ -128,54 +221,73 @@ class RichTextComposerLayout @JvmOverloads constructor( callback?.onAddAttachment() } - views.composerFullScreenButton.setOnClickListener { - callback?.onFullScreenModeChanged() + views.composerFullScreenButton.apply { + updateFullScreenButtonVisibility() + setOnClickListener { + callback?.onFullScreenModeChanged() + } } + views.composerEditTextOuterBorder.background = borderShapeDrawable + setupRichTextMenu() + + updateTextFieldBorder(isFullScreen) } private fun setupRichTextMenu() { addRichTextMenuItem( ElementR.drawable.ic_composer_bold, ElementR.string.rich_text_editor_format_bold, - ComposerAction.Bold + ComposerAction.BOLD ) { views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold) } addRichTextMenuItem( ElementR.drawable.ic_composer_italic, ElementR.string.rich_text_editor_format_italic, - ComposerAction.Italic + ComposerAction.ITALIC ) { views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic) } addRichTextMenuItem( ElementR.drawable.ic_composer_underlined, ElementR.string.rich_text_editor_format_underline, - ComposerAction.Underline + ComposerAction.UNDERLINE ) { views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline) } addRichTextMenuItem( ElementR.drawable.ic_composer_strikethrough, ElementR.string.rich_text_editor_format_strikethrough, - ComposerAction.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.menuStateChangedListener = - EditorEditText.OnMenuStateChangedListener { state -> - if (state is MenuState.Update) { - updateMenuStateFor(ComposerAction.Bold, state) - updateMenuStateFor(ComposerAction.Italic, state) - updateMenuStateFor(ComposerAction.Underline, state) - updateMenuStateFor(ComposerAction.StrikeThrough, state) + views.richTextComposerEditText.actionStatesChangedListener = + EditorEditText.OnActionStatesChangedListener { state -> + for (action in state.keys) { + updateMenuStateFor(action, state) } } @@ -186,6 +298,85 @@ class RichTextComposerLayout @JvmOverloads constructor( 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) } /** @@ -193,9 +384,9 @@ class RichTextComposerLayout @JvmOverloads constructor( */ private fun syncEditTexts() = if (isTextFormattingEnabled) { - views.plainTextComposerEditText.setText(views.richTextComposerEditText.getPlainText()) + views.plainTextComposerEditText.setText(views.richTextComposerEditText.getMarkdown()) } else { - views.richTextComposerEditText.setText(views.plainTextComposerEditText.text.toString()) + views.richTextComposerEditText.setMarkdown(views.plainTextComposerEditText.text.toString()) } private fun addRichTextMenuItem( @@ -216,91 +407,112 @@ class RichTextComposerLayout @JvmOverloads constructor( } } - private fun updateMenuStateFor(action: ComposerAction, menuState: MenuState.Update) { + private fun updateMenuStateFor( + action: ComposerAction, + menuState: Map + ) { val button = findViewWithTag(action) ?: return - button.isEnabled = !menuState.disabledActions.contains(action) - button.isSelected = menuState.reversedActions.contains(action) + val stateForAction = menuState[action] + button.isEnabled = stateForAction != ActionState.DISABLED + button.isSelected = stateForAction == ActionState.REVERSED } - private fun updateTextFieldBorder() { - val isExpanded = editText.editableText.lines().count() > 1 - val borderResource = if (isExpanded || isFullScreen) { - ElementR.drawable.bg_composer_rich_edit_text_expanded + 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 { - ElementR.drawable.bg_composer_rich_edit_text_single_line - } - views.composerEditTextOuterBorder.setBackgroundResource(borderResource) + resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line) + }.toFloat() + borderShapeDrawable.setCornerSize(cornerSize) } - override fun replaceFormattedContent(text: CharSequence) { + private fun replaceFormattedContent(text: CharSequence) { views.richTextComposerEditText.setHtml(text.toString()) - } - - override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) { - if (currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_compact) { - // ignore we good - return - } - currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_compact - applyNewConstraintSet(animate, transitionComplete) - updateEditTextVisibility() - } - - override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) { - if (currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_expanded) { - // ignore we good - return - } - currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_expanded - applyNewConstraintSet(animate, transitionComplete) - updateEditTextVisibility() + updateTextFieldBorder(isFullScreen) } override fun setTextIfDifferent(text: CharSequence?): Boolean { - return editText.setTextIfDifferent(text) - } - - override fun toggleFullScreen(newValue: Boolean) { - val constraintSetId = - if (newValue) R.layout.composer_rich_text_layout_constraint_set_fullscreen else currentConstraintSetId - ConstraintSet().also { - it.clone(context, constraintSetId) - it.applyTo(this) - } - - updateTextFieldBorder() - updateEditTextVisibility() - - updateEditTextFullScreenState(views.richTextComposerEditText, newValue) - updateEditTextFullScreenState(views.plainTextComposerEditText, newValue) + val result = editText.setTextIfDifferent(text) + updateTextFieldBorder(isFullScreen) + return result } private fun updateEditTextFullScreenState(editText: EditText, isFullScreen: Boolean) { if (isFullScreen) { editText.maxLines = Int.MAX_VALUE - // This is a workaround to fix incorrect scroll position when maximised - post { editText.requestLayout() } } else { - editText.maxLines = maxEditTextLinesWhenCollapsed + editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED } } - private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { - // val wasSendButtonInvisible = views.sendButton.isInvisible - if (animate) { - animateLayoutChange(animationDuration, transitionComplete) - } - ConstraintSet().also { - it.clone(context, currentConstraintSetId) - it.applyTo(this) + override fun renderComposerMode(mode: MessageComposerMode) { + 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 -> + if (isTextFormattingEnabled) { + replaceFormattedContent(text) + } else { + views.plainTextComposerEditText.setText(text) + } + } + views.sendButton.contentDescription = resources.getString(ElementR.string.action_send) + hasRelatedMessage = false } - // Might be updated by view state just after, but avoid blinks - // views.sendButton.isInvisible = wasSendButtonInvisible - } + views.sendButton.apply { + if (mode is MessageComposerMode.Edit) { + contentDescription = resources.getString(ElementR.string.action_save) + setImageResource(R.drawable.ic_composer_rich_text_save) + } else { + contentDescription = resources.getString(ElementR.string.action_send) + setImageResource(R.drawable.ic_rich_composer_send) + } + } - override fun setInvisible(isInvisible: Boolean) { - this.isInvisible = isInvisible + 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 -> { + // TODO We need sender info + // val senderInfo = mode.event.senderInfo + val userName = "TODO Sender name" // senderInfo.displayName ?: senderInfo.disambiguatedDisplayName + views.composerModeTitleView.text = + resources.getString(R.string.replying_to, userName) + views.composerModeIconView.setImageResource(R.drawable.ic_reply) + } + else -> Unit + } } private class TextChangeListener( diff --git a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/TextComposer.kt b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/TextComposer.kt index 506a91e1c4..fe616a085e 100644 --- a/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/src/main/java/io/element/android/x/textcomposer/TextComposer.kt @@ -55,8 +55,9 @@ fun TextComposer( } } + setFullScreen(fullscreen) (this as MessageComposerView).apply { - setup(fullscreen, isInDarkMode) + setup(isInDarkMode) } } }, @@ -68,14 +69,14 @@ fun TextComposer( // whenever the state changes // Example of Compose -> View communication val messageComposerView = (view as MessageComposerView) - messageComposerView.toggleFullScreen(fullscreen) + view.setFullScreen(fullscreen) messageComposerView.sendButton.isInvisible = !composerCanSendMessage messageComposerView.setTextIfDifferent(composerText ?: "") } ) } -private fun MessageComposerView.setup(fullscreen: Boolean, isDarkMode: Boolean) { +private fun MessageComposerView.setup(isDarkMode: Boolean) { val editTextColor = if(isDarkMode){ Color.WHITE }else{ @@ -83,7 +84,6 @@ private fun MessageComposerView.setup(fullscreen: Boolean, isDarkMode: Boolean) } editText.setTextColor(editTextColor) editText.setHintTextColor(editTextColor) - toggleFullScreen(fullscreen) editText.setHint(ElementR.string.room_message_placeholder) emojiButton?.isVisible = true sendButton.isVisible = true diff --git a/libraries/textcomposer/src/main/res/drawable/bg_composer_rich_bottom_sheet.xml b/libraries/textcomposer/src/main/res/drawable/bg_composer_rich_bottom_sheet.xml new file mode 100644 index 0000000000..47364373f7 --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/bg_composer_rich_bottom_sheet.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/libraries/textcomposer/src/main/res/drawable/bottomsheet_handle.xml b/libraries/textcomposer/src/main/res/drawable/bottomsheet_handle.xml new file mode 100644 index 0000000000..89ccf57ed0 --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/bottomsheet_handle.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/libraries/textcomposer/src/main/res/drawable/ic_composer_collapse.xml b/libraries/textcomposer/src/main/res/drawable/ic_composer_collapse.xml new file mode 100644 index 0000000000..724a833761 --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/ic_composer_collapse.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/textcomposer/src/main/res/drawable/ic_composer_full_screen.xml b/libraries/textcomposer/src/main/res/drawable/ic_composer_full_screen.xml new file mode 100644 index 0000000000..de1862c09b --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/ic_composer_full_screen.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/textcomposer/src/main/res/drawable/ic_composer_rich_text_editor_close.xml b/libraries/textcomposer/src/main/res/drawable/ic_composer_rich_text_editor_close.xml new file mode 100644 index 0000000000..c461470de5 --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/ic_composer_rich_text_editor_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/textcomposer/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml b/libraries/textcomposer/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml new file mode 100644 index 0000000000..4556974221 --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml @@ -0,0 +1,12 @@ + + + + diff --git a/libraries/textcomposer/src/main/res/drawable/ic_composer_rich_text_save.xml b/libraries/textcomposer/src/main/res/drawable/ic_composer_rich_text_save.xml new file mode 100644 index 0000000000..f270d6f8ae --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/ic_composer_rich_text_save.xml @@ -0,0 +1,16 @@ + + + + diff --git a/libraries/textcomposer/src/main/res/drawable/ic_quote.xml b/libraries/textcomposer/src/main/res/drawable/ic_quote.xml new file mode 100644 index 0000000000..0689651f1d --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/ic_quote.xml @@ -0,0 +1,14 @@ + + + diff --git a/libraries/textcomposer/src/main/res/drawable/ic_reply.xml b/libraries/textcomposer/src/main/res/drawable/ic_reply.xml new file mode 100644 index 0000000000..f23730624f --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/ic_reply.xml @@ -0,0 +1,11 @@ + + + + diff --git a/libraries/textcomposer/src/main/res/drawable/ic_rich_composer_add.xml b/libraries/textcomposer/src/main/res/drawable/ic_rich_composer_add.xml new file mode 100644 index 0000000000..3a90a40902 --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/ic_rich_composer_add.xml @@ -0,0 +1,15 @@ + + + + diff --git a/libraries/textcomposer/src/main/res/drawable/ic_rich_composer_send.xml b/libraries/textcomposer/src/main/res/drawable/ic_rich_composer_send.xml new file mode 100644 index 0000000000..0f99c1670e --- /dev/null +++ b/libraries/textcomposer/src/main/res/drawable/ic_rich_composer_send.xml @@ -0,0 +1,12 @@ + + + + diff --git a/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout.xml b/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout.xml index ae11ddb6a2..88f96c528e 100644 --- a/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout.xml +++ b/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout.xml @@ -1,185 +1,202 @@ - - - - - - - - - - - - - - - - - - - - - - + android:orientation="vertical" + android:background="@drawable/bg_composer_rich_bottom_sheet"> - - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> - - + - + - + - + - + + + + + android:layout_marginStart="6dp" + android:layout_marginTop="8dp" + android:paddingBottom="2dp" + android:fontFamily="sans-serif-medium" + tools:text="Editing" + style="@style/BottomSheetItemTime" + app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder" + app:layout_constraintStart_toEndOf="@id/composerModeIconView" /> - + - + - + - + + + + + + + + + + + + + + + + + + + diff --git a/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml b/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml deleted file mode 100644 index 1a3023a805..0000000000 --- a/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml b/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml deleted file mode 100644 index b0380d2e13..0000000000 --- a/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml +++ /dev/null @@ -1,230 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml b/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml deleted file mode 100644 index 3105063933..0000000000 --- a/libraries/textcomposer/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml +++ /dev/null @@ -1,234 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/libraries/textcomposer/src/main/res/values/dimens.xml b/libraries/textcomposer/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..7dea979fb9 --- /dev/null +++ b/libraries/textcomposer/src/main/res/values/dimens.xml @@ -0,0 +1,11 @@ + + + + 56dp + 52dp + 1dp + 28dp + 14dp + 44dp + + \ No newline at end of file diff --git a/libraries/textcomposer/src/main/res/values/strings.xml b/libraries/textcomposer/src/main/res/values/strings.xml new file mode 100644 index 0000000000..04b06a4eb7 --- /dev/null +++ b/libraries/textcomposer/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + + Editing + Replying to %s + Quoting + + \ No newline at end of file