diff --git a/changelog.d/7577.feature b/changelog.d/7577.feature
new file mode 100644
index 0000000000..e21ccb13c0
--- /dev/null
+++ b/changelog.d/7577.feature
@@ -0,0 +1 @@
+New implementation of the full screen mode for the Rich Text Editor.
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index e503cb3fe7..ab98f7e141 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -1642,7 +1642,10 @@
It looks like you’re trying to connect to another homeserver. Do you want to sign out?
Edit
+ Editing
Reply
+ Replying to %s
+ Quoting
Reply in thread
View In Room
diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml
index 22c2a3e62c..4c911c9e97 100644
--- a/library/ui-styles/src/main/res/values/dimens.xml
+++ b/library/ui-styles/src/main/res/values/dimens.xml
@@ -49,6 +49,7 @@
1dp
28dp
14dp
+ 44dp
28dp
6dp
diff --git a/library/ui-styles/src/main/res/values/styles_edit_text.xml b/library/ui-styles/src/main/res/values/styles_edit_text.xml
index b640fc49d9..94f4d86160 100644
--- a/library/ui-styles/src/main/res/values/styles_edit_text.xml
+++ b/library/ui-styles/src/main/res/values/styles_edit_text.xml
@@ -4,7 +4,7 @@
diff --git a/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt
new file mode 100644
index 0000000000..0474cdea7e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt
@@ -0,0 +1,791 @@
+package im.vector.app.core.utils
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.VelocityTracker
+import android.view.View
+import android.view.View.MeasureSpec
+import android.view.ViewGroup
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import androidx.core.view.updatePadding
+import androidx.customview.widget.ViewDragHelper
+import com.google.android.material.appbar.AppBarLayout
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import timber.log.Timber
+import java.lang.ref.WeakReference
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * BottomSheetBehavior that dynamically resizes its contents as it grows or shrinks.
+ * Most of the nested scrolling and touch events code is the same as in [BottomSheetBehavior], but we couldn't just extend it.
+ */
+class ExpandingBottomSheetBehavior : CoordinatorLayout.Behavior {
+
+ companion object {
+ /** Gets a [ExpandingBottomSheetBehavior] from the passed [view] if it exists. */
+ @Suppress("UNCHECKED_CAST")
+ fun from(view: V): ExpandingBottomSheetBehavior? {
+ val params = view.layoutParams as? CoordinatorLayout.LayoutParams ?: return null
+ return params.behavior as? ExpandingBottomSheetBehavior
+ }
+ }
+
+ /** [Callback] to notify changes in dragging state and position. */
+ interface Callback {
+ /** Called when the dragging state of the BottomSheet changes. */
+ fun onStateChanged(state: State) {}
+
+ /** Called when the position of the BottomSheet changes while dragging. */
+ fun onSlidePositionChanged(view: View, yPosition: Float) {}
+ }
+
+ /** Represents the 4 possible states of the BottomSheet. */
+ enum class State(val value: Int) {
+ /** BottomSheet is at min height, collapsed at the bottom. */
+ Collapsed(0),
+
+ /** BottomSheet is being dragged by the user. */
+ Dragging(1),
+
+ /** BottomSheet has been released after being dragged by the user and is animating to its destination. */
+ Settling(2),
+
+ /** BottomSheet is at its max height. */
+ Expanded(3);
+
+ /** Returns whether the BottomSheet is being dragged or is settling after being dragged. */
+ fun isDraggingOrSettling(): Boolean = this == Dragging || this == Settling
+ }
+
+ /** Set to true to enable debug logging of sizes and offsets. Defaults to `false`. */
+ var enableDebugLogs = false
+
+ /** Current BottomSheet state. Default to [State.Collapsed]. */
+ var state: State = State.Collapsed
+ private set
+
+ /** Whether the BottomSheet can be dragged by the user or not. Defaults to `true`. */
+ var isDraggable = true
+
+ /** [Callback] to notify changes in dragging state and position. */
+ var callback: Callback? = null
+ set(value) {
+ field = value
+ // Send initial state
+ value?.onStateChanged(state)
+ }
+
+ /** Additional top offset in `dps` to add to the BottomSheet so it doesn't fill the whole screen. Defaults to `0`. */
+ var topOffset = 0
+ set(value) {
+ field = value
+ expandedOffset = -1
+ }
+
+ /** Whether the BottomSheet should be expanded up to the bottom of any [AppBarLayout] found in the parent [CoordinatorLayout]. Defaults to `false`. */
+ var avoidAppBarLayout = false
+ set(value) {
+ field = value
+ expandedOffset = -1
+ }
+
+ /**
+ * Whether to add the [scrimView], a 'shadow layer' that will be displayed while dragging/expanded so it obscures the content below the BottomSheet.
+ * Defaults to `false`.
+ */
+ var useScrimView = false
+
+ /** Color to use for the [scrimView] shadow layer. */
+ var scrimViewColor = 0x60000000
+
+ /** [View.TRANSLATION_Z] in `dps` to apply to the [scrimView]. Defaults to `0dp`. */
+ var scrimViewTranslationZ = 0
+
+ /** Whether the content view should be layout to the top of the BottomSheet when it's collapsed. Defaults to true. */
+ var applyInsetsToContentViewWhenCollapsed = true
+
+ /** Lambda used to calculate a min collapsed when the view using the behavior should have a special 'collapsed' layout. It's null by default. */
+ var minCollapsedHeight: (() -> Int)? = null
+
+ // Internal BottomSheet implementation properties
+ private var ignoreEvents = false
+ private var touchingScrollingChild = false
+
+ private var lastY: Int = -1
+ private var collapsedOffset = -1
+ private var expandedOffset = -1
+ private var parentWidth = -1
+ private var parentHeight = -1
+
+ private var activePointerId = -1
+
+ private var lastNestedScrollDy = -1
+ private var isNestedScrolled = false
+
+ private var viewRef: WeakReference? = null
+ private var nestedScrollingChildRef: WeakReference? = null
+ private var velocityTracker: VelocityTracker? = null
+
+ private var dragHelper: ViewDragHelper? = null
+ private var scrimView: View? = null
+
+ private val stateSettlingTracker = StateSettlingTracker()
+ private var prevState: State? = null
+
+ private var insetBottom = 0
+ private var insetTop = 0
+ private var insetLeft = 0
+ private var insetRight = 0
+
+ private var initialPaddingTop = 0
+ private var initialPaddingBottom = 0
+ private var initialPaddingLeft = 0
+ private var initialPaddingRight = 0
+ private val minCollapsedOffset: Int?
+ get() {
+ val minHeight = minCollapsedHeight?.invoke() ?: return null
+ if (minHeight == -1) return null
+ return parentHeight - minHeight - insetBottom
+ }
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor() : super()
+
+ override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean {
+ parentWidth = parent.width
+ parentHeight = parent.height
+
+ if (viewRef == null) {
+ viewRef = WeakReference(child)
+ setWindowInsetsListener(child)
+ // Prevents clicking on overlapped items below the BottomSheet
+ child.isClickable = true
+ }
+
+ parent.updatePadding(left = insetLeft, right = insetRight)
+
+ ensureViewDragHelper(parent)
+
+ // Top coordinate before this layout pass
+ val savedTop = child.top
+
+ // Calculate default position of the BottomSheet's children
+ parent.onLayoutChild(child, layoutDirection)
+
+ // This should optimise calculations when they're not needed
+ if (state == State.Collapsed) {
+ calculateCollapsedOffset(child)
+ }
+ calculateExpandedOffset(parent)
+
+ // Apply top and bottom insets to contentView if needed
+ val appBar = findAppBarLayout(parent)
+ val contentView = parent.children.find { it !== appBar && it !== child && it !== scrimView }
+ if (applyInsetsToContentViewWhenCollapsed && state == State.Collapsed && contentView != null) {
+ val topOffset = appBar?.measuredHeight ?: 0
+ val bottomOffset = parentHeight - collapsedOffset + insetTop
+ val params = contentView.layoutParams as CoordinatorLayout.LayoutParams
+ if (params.bottomMargin != bottomOffset || params.topMargin != topOffset) {
+ params.topMargin = topOffset
+ params.bottomMargin = bottomOffset
+ contentView.layoutParams = params
+ }
+ }
+
+ // Add scrimView if needed
+ if (useScrimView && scrimView == null) {
+ val scrimView = View(parent.context)
+ scrimView.setBackgroundColor(scrimViewColor)
+ scrimView.translationZ = scrimViewTranslationZ * child.resources.displayMetrics.scaledDensity
+ scrimView.isVisible = false
+ val params = CoordinatorLayout.LayoutParams(
+ CoordinatorLayout.LayoutParams.MATCH_PARENT,
+ CoordinatorLayout.LayoutParams.MATCH_PARENT
+ )
+ scrimView.layoutParams = params
+ val currentIndex = parent.children.indexOf(child)
+ parent.addView(scrimView, currentIndex)
+ this.scrimView = scrimView
+ } else if (!useScrimView && scrimView != null) {
+ parent.removeView(scrimView)
+ scrimView = null
+ }
+
+ // Apply insets and resize child based on the current State
+ when (state) {
+ State.Collapsed -> {
+ scrimView?.alpha = 0f
+ val newHeight = parentHeight - collapsedOffset + insetTop
+ val params = child.layoutParams
+ if (params.height != newHeight) {
+ params.height = newHeight
+ child.layoutParams = params
+ }
+ // If the offset is < insetTop it will cover the status bar too
+ val newOffset = max(insetTop, collapsedOffset - insetTop)
+ ViewCompat.offsetTopAndBottom(child, newOffset)
+ log("State: Collapsed | Offset: $newOffset | Height: $newHeight")
+ }
+ State.Dragging, State.Settling -> {
+ val newOffset = savedTop - child.top
+ val percentage = max(0f, 1f - (newOffset.toFloat() / collapsedOffset.toFloat()))
+ scrimView?.let {
+ if (percentage == 0f) {
+ it.isVisible = false
+ } else {
+ it.alpha = percentage
+ it.isVisible = true
+ }
+ }
+ val params = child.layoutParams
+ params.height = parentHeight - savedTop
+ child.layoutParams = params
+ ViewCompat.offsetTopAndBottom(child, newOffset)
+ val stateStr = if (state == State.Dragging) "Dragging" else "Settling"
+ log("State: $stateStr | Offset: $newOffset | Percentage: $percentage")
+ }
+ State.Expanded -> {
+ val params = child.layoutParams
+ val newHeight = parentHeight - expandedOffset
+ if (params.height != newHeight) {
+ params.height = newHeight
+ child.layoutParams = params
+ }
+ ViewCompat.offsetTopAndBottom(child, expandedOffset)
+ log("State: Expanded | Offset: $expandedOffset | Height: $newHeight")
+ }
+ }
+
+ // Find a nested scrolling child to take into account for touch events
+ if (nestedScrollingChildRef == null) {
+ nestedScrollingChildRef = findScrollingChild(child)?.let { WeakReference(it) }
+ }
+
+ return true
+ }
+
+ // region: Touch events
+ override fun onInterceptTouchEvent(
+ parent: CoordinatorLayout,
+ child: V,
+ ev: MotionEvent
+ ): Boolean {
+ // Almost everything inside here is verbatim to BottomSheetBehavior's onTouchEvent
+ if (viewRef != null && viewRef?.get() !== child) {
+ return true
+ }
+ val action = ev.actionMasked
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ resetTouchEventTracking()
+ }
+ if (velocityTracker == null) {
+ velocityTracker = VelocityTracker.obtain()
+ }
+ velocityTracker?.addMovement(ev)
+
+ when (action) {
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ touchingScrollingChild = false
+ activePointerId = MotionEvent.INVALID_POINTER_ID
+ if (ignoreEvents) {
+ ignoreEvents = false
+ return false
+ }
+ }
+ MotionEvent.ACTION_DOWN -> {
+ val x = ev.x.toInt()
+ lastY = ev.y.toInt()
+
+ // Only intercept nested scrolling events here if the view not being moved by the
+ // ViewDragHelper.
+ val scroll = nestedScrollingChildRef?.get()
+ if (state != State.Settling) {
+ if (scroll != null && parent.isPointInChildBounds(scroll, x, lastY)) {
+ activePointerId = ev.getPointerId(ev.actionIndex)
+ touchingScrollingChild = true
+ }
+ }
+ ignoreEvents = (activePointerId == MotionEvent.INVALID_POINTER_ID &&
+ !parent.isPointInChildBounds(child, x, lastY))
+ }
+ else -> Unit
+ }
+
+ if (!ignoreEvents && isDraggable && dragHelper?.shouldInterceptTouchEvent(ev) == true) {
+ return true
+ }
+
+ // If using scrim view, a click on it should collapse the bottom sheet
+ if (useScrimView && state == State.Expanded && action == MotionEvent.ACTION_DOWN) {
+ val y = ev.y.toInt()
+ if (y <= expandedOffset) {
+ setState(State.Collapsed)
+ return true
+ }
+ }
+
+ // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because
+ // it is not the top most view of its parent. This is not necessary when the touch event is
+ // happening over the scrolling content as nested scrolling logic handles that case.
+ val scroll = nestedScrollingChildRef?.get()
+ return (action == MotionEvent.ACTION_MOVE &&
+ scroll != null &&
+ !ignoreEvents &&
+ state != State.Dragging &&
+ !parent.isPointInChildBounds(scroll, ev.x.toInt(), ev.y.toInt()) &&
+ dragHelper != null &&
+ abs(lastY - ev.y.toInt()) > (dragHelper?.touchSlop ?: 0))
+ }
+
+ override fun onTouchEvent(parent: CoordinatorLayout, child: V, ev: MotionEvent): Boolean {
+ // Almost everything inside here is verbatim to BottomSheetBehavior's onTouchEvent
+ val action = ev.actionMasked
+ if (state == State.Dragging && action == MotionEvent.ACTION_DOWN) {
+ return true
+ }
+ if (shouldHandleDraggingWithHelper()) {
+ dragHelper?.processTouchEvent(ev)
+ }
+
+ // Record the velocity
+ if (action == MotionEvent.ACTION_DOWN) {
+ resetTouchEventTracking()
+ }
+ if (velocityTracker == null) {
+ velocityTracker = VelocityTracker.obtain()
+ }
+ velocityTracker?.addMovement(ev)
+
+ if (shouldHandleDraggingWithHelper() && action == MotionEvent.ACTION_MOVE && !ignoreEvents) {
+ if (abs(lastY - ev.y.toInt()) > (dragHelper?.touchSlop ?: 0)) {
+ dragHelper?.captureChildView(child, ev.getPointerId(ev.actionIndex))
+ }
+ }
+
+ return !ignoreEvents
+ }
+
+ private fun resetTouchEventTracking() {
+ activePointerId = ViewDragHelper.INVALID_POINTER
+ velocityTracker?.recycle()
+ velocityTracker = null
+ }
+ // endregion
+
+ override fun onAttachedToLayoutParams(params: CoordinatorLayout.LayoutParams) {
+ super.onAttachedToLayoutParams(params)
+
+ viewRef = null
+ dragHelper = null
+ }
+
+ override fun onDetachedFromLayoutParams() {
+ super.onDetachedFromLayoutParams()
+
+ viewRef = null
+ dragHelper = null
+ }
+
+ // region: Size measuring and utils
+ private fun calculateCollapsedOffset(child: View) {
+ val availableSpace = parentHeight - insetTop
+ child.measure(
+ MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(availableSpace, MeasureSpec.AT_MOST),
+ )
+ collapsedOffset = parentHeight - child.measuredHeight + insetTop
+ }
+
+ private fun calculateExpandedOffset(parent: CoordinatorLayout): Int {
+ expandedOffset = if (avoidAppBarLayout) {
+ findAppBarLayout(parent)?.measuredHeight ?: 0
+ } else {
+ 0
+ } + topOffset + insetTop
+ return expandedOffset
+ }
+
+ private fun ensureViewDragHelper(parent: CoordinatorLayout) {
+ if (dragHelper == null) {
+ dragHelper = ViewDragHelper.create(parent, dragHelperCallback)
+ }
+ }
+
+ private fun findAppBarLayout(view: View): AppBarLayout? {
+ return when (view) {
+ is AppBarLayout -> view
+ is ViewGroup -> view.children.firstNotNullOfOrNull { findAppBarLayout(it) }
+ else -> null
+ }
+ }
+
+ private fun shouldHandleDraggingWithHelper(): Boolean {
+ return dragHelper != null && (isDraggable || state == State.Dragging)
+ }
+
+ private fun log(contents: String, vararg args: Any) {
+ if (!enableDebugLogs) return
+ Timber.d(contents, args)
+ }
+ // endregion
+
+ // region: State and delayed state settling
+ fun setState(state: State) {
+ if (state == this.state) {
+ return
+ } else if (viewRef?.get() == null) {
+ setInternalState(state)
+ } else {
+ viewRef?.get()?.let { child ->
+ runAfterLayout(child) { startSettling(child, state, false) }
+ }
+ }
+ }
+
+ private fun setInternalState(state: State) {
+ if (!this.state.isDraggingOrSettling()) {
+ prevState = this.state
+ }
+ this.state = state
+
+ viewRef?.get()?.requestLayout()
+
+ callback?.onStateChanged(state)
+ }
+
+ private fun startSettling(child: View, state: State, isReleasingView: Boolean) {
+ val top = getTopOffsetForState(state)
+ log("Settling to: $top")
+ val isSettling = dragHelper?.let {
+ if (isReleasingView) {
+ it.settleCapturedViewAt(child.left, top)
+ } else {
+ it.smoothSlideViewTo(child, child.left, top)
+ }
+ } ?: false
+ setInternalState(if (isSettling) State.Settling else state)
+
+ if (isSettling) {
+ stateSettlingTracker.continueSettlingToState(state)
+ }
+ }
+
+ private fun runAfterLayout(child: V, runnable: Runnable) {
+ if (isLayouting(child)) {
+ child.post(runnable)
+ } else {
+ runnable.run()
+ }
+ }
+
+ private fun isLayouting(child: V): Boolean {
+ return child.parent != null && child.parent.isLayoutRequested && ViewCompat.isAttachedToWindow(child)
+ }
+
+ private fun getTopOffsetForState(state: State): Int {
+ return when (state) {
+ State.Collapsed -> minCollapsedOffset ?: collapsedOffset
+ State.Expanded -> expandedOffset
+ else -> error("Cannot get offset for state $state")
+ }
+ }
+ // endregion
+
+ // region: Nested scroll
+ override fun onStartNestedScroll(
+ coordinatorLayout: CoordinatorLayout,
+ child: V,
+ directTargetChild: View,
+ target: View,
+ axes: Int,
+ type: Int
+ ): Boolean {
+ lastNestedScrollDy = 0
+ isNestedScrolled = false
+ return (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0
+ }
+
+ override fun onNestedPreScroll(
+ coordinatorLayout: CoordinatorLayout,
+ child: V,
+ target: View,
+ dx: Int,
+ dy: Int,
+ consumed: IntArray,
+ type: Int
+ ) {
+ if (type == ViewCompat.TYPE_NON_TOUCH) return
+ val scrollingChild = nestedScrollingChildRef?.get()
+ if (target != scrollingChild) return
+
+ val currentTop = child.top
+ val newTop = currentTop - dy
+ if (dy > 0) {
+ // Upward scroll
+ if (newTop < expandedOffset) {
+ consumed[1] = currentTop - expandedOffset
+ ViewCompat.offsetTopAndBottom(child, -consumed[1])
+ setInternalState(State.Expanded)
+ } else {
+ if (!isDraggable) return
+
+ consumed[1] = dy
+ ViewCompat.offsetTopAndBottom(child, -dy)
+ setInternalState(State.Dragging)
+ }
+ } else if (dy < 0) {
+ // Scroll downward
+ if (!target.canScrollVertically(-1)) {
+ if (newTop <= collapsedOffset) {
+ if (!isDraggable) return
+
+ consumed[1] = dy
+ ViewCompat.offsetTopAndBottom(child, -dy)
+ setInternalState(State.Dragging)
+ } else {
+ consumed[1] = currentTop - collapsedOffset
+ ViewCompat.offsetTopAndBottom(child, -consumed[1])
+ setInternalState(State.Collapsed)
+ }
+ }
+ }
+ lastNestedScrollDy = dy
+ isNestedScrolled = true
+ }
+
+ override fun onNestedScroll(
+ coordinatorLayout: CoordinatorLayout,
+ child: V,
+ target: View,
+ dxConsumed: Int,
+ dyConsumed: Int,
+ dxUnconsumed: Int,
+ dyUnconsumed: Int,
+ type: Int,
+ consumed: IntArray
+ ) {
+ // Empty to avoid default behaviour
+ }
+
+ override fun onNestedPreFling(
+ coordinatorLayout: CoordinatorLayout,
+ child: V,
+ target: View,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ return target == nestedScrollingChildRef?.get() &&
+ (state != State.Expanded || super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY))
+ }
+
+ private fun findScrollingChild(view: View): View? {
+ return when {
+ !view.isVisible -> null
+ ViewCompat.isNestedScrollingEnabled(view) -> view
+ view is ViewGroup -> {
+ view.children.firstNotNullOfOrNull { findScrollingChild(it) }
+ }
+ else -> null
+ }
+ }
+ // endregion
+
+ // region: Insets
+ private fun setWindowInsetsListener(view: View) {
+ // Create a snapshot of the view's padding state.
+ initialPaddingLeft = view.paddingLeft
+ initialPaddingTop = view.paddingTop
+ initialPaddingRight = view.paddingRight
+ initialPaddingBottom = view.paddingBottom
+
+ // This should only be used to set initial insets and other edge cases where the insets can't be applied using an animation.
+ var applyInsetsFromAnimation = false
+
+ // This will animated inset changes, making them look a lot better. However, it won't update initial insets.
+ ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
+ override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList): WindowInsetsCompat {
+ return applyInsets(view, insets)
+ }
+
+ override fun onEnd(animation: WindowInsetsAnimationCompat) {
+ applyInsetsFromAnimation = false
+ view.requestApplyInsets()
+ }
+ })
+
+ ViewCompat.setOnApplyWindowInsetsListener(view) { _: View, insets: WindowInsetsCompat ->
+ if (!applyInsetsFromAnimation) {
+ applyInsetsFromAnimation = true
+ applyInsets(view, insets)
+ } else {
+ insets
+ }
+ }
+
+ // Request to apply insets as soon as the view is attached to a window.
+ if (ViewCompat.isAttachedToWindow(view)) {
+ ViewCompat.requestApplyInsets(view)
+ } else {
+ view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) {
+ v.removeOnAttachStateChangeListener(this)
+ ViewCompat.requestApplyInsets(v)
+ }
+
+ override fun onViewDetachedFromWindow(v: View) = Unit
+ })
+ }
+ }
+
+ private fun applyInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ val insetsType = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()
+ val imeInsets = insets.getInsets(insetsType)
+ insetTop = imeInsets.top
+ insetBottom = imeInsets.bottom
+ insetLeft = imeInsets.left
+ insetRight = imeInsets.right
+
+ val bottomPadding = initialPaddingBottom + insetBottom
+ view.setPadding(initialPaddingLeft, initialPaddingTop, initialPaddingRight, bottomPadding)
+ if (state == State.Collapsed) {
+ val params = view.layoutParams
+ params.height = CoordinatorLayout.LayoutParams.WRAP_CONTENT
+ view.layoutParams = params
+ calculateCollapsedOffset(view)
+ }
+ return WindowInsetsCompat.CONSUMED
+ }
+ // endregion
+
+ // Used to add dragging animations along with StateSettlingTracker, and set max and min dragging coordinates.
+ private val dragHelperCallback = object : ViewDragHelper.Callback() {
+
+ override fun tryCaptureView(child: View, pointerId: Int): Boolean {
+ if (state == State.Dragging) {
+ return false
+ }
+
+ if (touchingScrollingChild) {
+ return false
+ }
+
+ if (state == State.Expanded && activePointerId == pointerId) {
+ val scroll = nestedScrollingChildRef?.get()
+ if (scroll?.canScrollVertically(-1) == true) {
+ return false
+ }
+ }
+
+ return viewRef?.get() == child
+ }
+
+ override fun onViewDragStateChanged(state: Int) {
+ if (state == ViewDragHelper.STATE_DRAGGING && isDraggable) {
+ setInternalState(State.Dragging)
+ }
+ }
+
+ override fun onViewPositionChanged(
+ changedView: View,
+ left: Int,
+ top: Int,
+ dx: Int,
+ dy: Int
+ ) {
+ super.onViewPositionChanged(changedView, left, top, dx, dy)
+
+ val params = changedView.layoutParams
+ params.height = parentHeight - top + insetBottom + insetTop
+ changedView.layoutParams = params
+
+ val collapsedOffset = minCollapsedOffset ?: collapsedOffset
+ val percentage = 1f - (top - insetTop).toFloat() / collapsedOffset.toFloat()
+
+ callback?.onSlidePositionChanged(changedView, percentage)
+ }
+
+ override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
+ val actualCollapsedOffset = minCollapsedOffset ?: collapsedOffset
+ val targetState = if (yvel < 0) {
+ // Moving up
+ val currentTop = releasedChild.top
+
+ val yPositionPercentage = currentTop * 100f / actualCollapsedOffset
+ if (yPositionPercentage >= 0.5f) {
+ State.Expanded
+ } else {
+ State.Collapsed
+ }
+ } else if (yvel == 0f || abs(xvel) > abs(yvel)) {
+ // If the Y velocity is 0 or the swipe was mostly horizontal indicated by the X velocity
+ // being greater than the Y velocity, settle to the nearest correct height.
+
+ val currentTop = releasedChild.top
+ if (currentTop < actualCollapsedOffset / 2) {
+ State.Expanded
+ } else {
+ State.Collapsed
+ }
+ } else {
+ // Moving down
+ val currentTop = releasedChild.top
+
+ val yPositionPercentage = currentTop * 100f / actualCollapsedOffset
+ if (yPositionPercentage >= 0.5f) {
+ State.Collapsed
+ } else {
+ State.Expanded
+ }
+ }
+ startSettling(releasedChild, targetState, true)
+ }
+
+ override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
+ return child.left
+ }
+
+ override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
+ val collapsed = minCollapsedOffset ?: collapsedOffset
+ val maxTop = max(top, insetTop)
+ return min(max(maxTop, expandedOffset), collapsed)
+ }
+
+ override fun getViewVerticalDragRange(child: View): Int {
+ return minCollapsedOffset ?: collapsedOffset
+ }
+ }
+
+ // Used to set the current State in a delayed way.
+ private inner class StateSettlingTracker {
+ private lateinit var targetState: State
+ private var isContinueSettlingRunnablePosted = false
+
+ private val continueSettlingRunnable: Runnable = Runnable {
+ isContinueSettlingRunnablePosted = false
+ if (dragHelper?.continueSettling(true) == true) {
+ continueSettlingToState(targetState)
+ } else {
+ setInternalState(targetState)
+ }
+ }
+
+ fun continueSettlingToState(state: State) {
+ val view = viewRef?.get() ?: return
+
+ this.targetState = state
+ if (!isContinueSettlingRunnablePosted) {
+ ViewCompat.postOnAnimation(view, continueSettlingRunnable)
+ isContinueSettlingRunnablePosted = true
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
index 1368b71ec6..0f7dc251ae 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt
@@ -34,8 +34,6 @@ class JumpToBottomViewVisibilityManager(
private val layoutManager: LinearLayoutManager
) {
- private var canShowButtonOnScroll = true
-
init {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@@ -45,7 +43,7 @@ class JumpToBottomViewVisibilityManager(
if (scrollingToPast) {
jumpToBottomView.hide()
- } else if (canShowButtonOnScroll) {
+ } else {
maybeShowJumpToBottomViewVisibility()
}
}
@@ -68,13 +66,7 @@ class JumpToBottomViewVisibilityManager(
}
}
- fun hideAndPreventVisibilityChangesWithScrolling() {
- jumpToBottomView.hide()
- canShowButtonOnScroll = false
- }
-
private fun maybeShowJumpToBottomViewVisibility() {
- canShowButtonOnScroll = true
if (layoutManager.findFirstVisibleItemPosition() > 1) {
jumpToBottomView.show()
} else {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
index ecbea133df..2ed3bf8614 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
@@ -18,10 +18,12 @@ package im.vector.app.features.home.room.detail
import android.content.Context
import android.content.Intent
+import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.view.GravityCompat
+import androidx.core.view.WindowCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
@@ -98,6 +100,11 @@ class RoomDetailActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
+ // For dealing with insets and status bar background color
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ window.statusBarColor = Color.TRANSPARENT
+
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
waitingView = views.waitingView.waitingView
val timelineArgs: TimelineArgs = intent?.extras?.getParcelableCompat(EXTRA_ROOM_DETAIL_ARGS) ?: return
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index e1392b7580..9bed0aae04 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -35,16 +35,17 @@ import android.widget.TextView
import androidx.activity.addCallback
import androidx.annotation.StringRes
import androidx.appcompat.view.menu.MenuBuilder
-import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.net.toUri
import androidx.core.text.toSpannable
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
+import androidx.core.view.updatePadding
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
@@ -67,7 +68,6 @@ import im.vector.app.core.dialogs.ConfirmationDialogBuilder
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelperFactory
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
-import im.vector.app.core.extensions.animateLayoutChange
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.containsRtLOverride
@@ -187,9 +187,7 @@ import im.vector.app.features.widgets.WidgetArgs
import im.vector.app.features.widgets.WidgetKind
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -418,20 +416,12 @@ class TimelineFragment :
}
}
- if (savedInstanceState == null) {
- handleSpaceShare()
+ ViewCompat.setOnApplyWindowInsetsListener(views.coordinatorLayout) { _, insets ->
+ val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars())
+ views.appBarLayout.updatePadding(top = imeInsets.top)
+ views.voiceMessageRecorderContainer.updatePadding(bottom = imeInsets.bottom)
+ insets
}
-
- views.scrim.setOnClickListener {
- messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(false))
- }
-
- messageComposerViewModel.stateFlow.map { it.isFullScreen }
- .distinctUntilChanged()
- .onEach { isFullScreen ->
- toggleFullScreenEditor(isFullScreen)
- }
- .launchIn(viewLifecycleOwner.lifecycleScope)
}
private fun setupBackPressHandling() {
@@ -1048,13 +1038,7 @@ class TimelineFragment :
override fun onLayoutCompleted(state: RecyclerView.State) {
super.onLayoutCompleted(state)
updateJumpToReadMarkerViewVisibility()
- withState(messageComposerViewModel) { composerState ->
- if (!composerState.isFullScreen) {
- jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
- } else {
- jumpToBottomViewVisibilityManager.hideAndPreventVisibilityChangesWithScrolling()
- }
- }
+ jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
}
}.apply {
// For local rooms, pin the view's content to the top edge (the layout is reversed)
@@ -1170,7 +1154,6 @@ class TimelineFragment :
if (mainState.tombstoneEvent == null) {
views.composerContainer.isInvisible = !messageComposerState.isComposerVisible
views.voiceMessageRecorderContainer.isVisible = messageComposerState.isVoiceMessageRecorderVisible
-
when (messageComposerState.canSendMessage) {
CanSendStatus.Allowed -> {
NotificationAreaView.State.Hidden
@@ -2036,19 +2019,6 @@ class TimelineFragment :
}
}
- private fun toggleFullScreenEditor(isFullScreen: Boolean) {
- views.composerContainer.animateLayoutChange(200)
-
- val constraintSet = ConstraintSet()
- val constraintSetId = if (isFullScreen) {
- R.layout.fragment_timeline_fullscreen
- } else {
- R.layout.fragment_timeline
- }
- constraintSet.clone(requireContext(), constraintSetId)
- constraintSet.applyTo(views.rootConstraintLayout)
- }
-
/**
* Returns true if the current room is a Thread room, false otherwise.
*/
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
index 30437a016d..ffaaa235cf 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt
@@ -33,7 +33,6 @@ sealed class MessageComposerAction : VectorViewModelAction {
data class OnEntersBackground(val composerText: String) : MessageComposerAction()
data class SlashCommandConfirmed(val parsedCommand: ParsedCommand) : MessageComposerAction()
data class InsertUserDisplayName(val userId: String) : MessageComposerAction()
-
data class SetFullScreen(val isFullScreen: Boolean) : MessageComposerAction()
// Voice Message
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
index aaf63d7f41..d551850ff3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt
@@ -24,7 +24,6 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.Spannable
-import android.text.format.DateUtils
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
@@ -32,10 +31,7 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.Toast
-import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
-import androidx.annotation.StringRes
-import androidx.core.content.ContextCompat
import androidx.core.text.buildSpannedString
import androidx.core.view.isGone
import androidx.core.view.isInvisible
@@ -51,7 +47,6 @@ import com.vanniktech.emoji.EmojiPopup
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.error.fatalError
-import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.glide.GlideApp
@@ -59,7 +54,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.lifecycleAwareLazy
import im.vector.app.core.platform.showOptimizedSnackbar
import im.vector.app.core.resources.BuildMeta
-import im.vector.app.core.utils.DimensionConverter
+import im.vector.app.core.utils.ExpandingBottomSheetBehavior
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.registerForPermissionsResult
@@ -86,14 +81,9 @@ import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAc
import im.vector.app.features.home.room.detail.TimelineViewModel
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
-import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
-import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
-import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillImageSpan
-import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.location.LocationSharingMode
-import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.poll.PollMode
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.share.SharedData
@@ -104,18 +94,9 @@ import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
-import org.commonmark.parser.Parser
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
-import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
-import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.MatrixItem
-import org.matrix.android.sdk.api.util.toMatrixItem
import reactivecircus.flowbinding.android.view.focusChanges
import reactivecircus.flowbinding.android.widget.textChanges
import timber.log.Timber
@@ -130,12 +111,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
@Inject lateinit var autoCompleterFactory: AutoCompleter.Factory
@Inject lateinit var avatarRenderer: AvatarRenderer
- @Inject lateinit var matrixItemColorProvider: MatrixItemColorProvider
- @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
- @Inject lateinit var dimensionConverter: DimensionConverter
- @Inject lateinit var imageContentRenderer: ImageContentRenderer
@Inject lateinit var shareIntentHandler: ShareIntentHandler
- @Inject lateinit var pillsPostProcessorFactory: PillsPostProcessor.Factory
@Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var vectorFeatures: VectorFeatures
@Inject lateinit var buildMeta: BuildMeta
@@ -147,10 +123,6 @@ class MessageComposerFragment : VectorBaseFragment(), A
autoCompleterFactory.create(roomId, isThreadTimeLine())
}
- private val pillsPostProcessor by lazy {
- pillsPostProcessorFactory.create(roomId)
- }
-
private val emojiPopup: EmojiPopup by lifecycleAwareLazy {
createEmojiPopup()
}
@@ -166,6 +138,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
private lateinit var attachmentsHelper: AttachmentsHelper
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
+ private var bottomSheetBehavior: ExpandingBottomSheetBehavior? = null
private val timelineViewModel: TimelineViewModel by parentFragmentViewModel()
private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
@@ -192,6 +165,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
attachmentsHelper = AttachmentsHelper(requireContext(), this, buildMeta).register()
+ setupBottomSheet()
setupComposer()
setupEmojiButton()
@@ -217,22 +191,15 @@ class MessageComposerFragment : VectorBaseFragment(), A
}
}
- messageComposerViewModel.stateFlow.map { it.isFullScreen }
- .distinctUntilChanged()
- .onEach { isFullScreen ->
- composer.toggleFullScreen(isFullScreen)
- }
- .launchIn(viewLifecycleOwner.lifecycleScope)
-
messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend ->
if (!canSend.boolean()) {
return@onEach
}
when (mode) {
is SendMode.Regular -> renderRegularMode(mode.text.toString())
- is SendMode.Edit -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text.toString())
- is SendMode.Quote -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.action_quote, mode.text.toString())
- is SendMode.Reply -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text.toString())
+ is SendMode.Edit -> renderSpecialMode(MessageComposerMode.Edit(mode.timelineEvent, mode.text.toString()))
+ is SendMode.Quote -> renderSpecialMode(MessageComposerMode.Quote(mode.timelineEvent, mode.text.toString()))
+ is SendMode.Reply -> renderSpecialMode(MessageComposerMode.Reply(mode.timelineEvent, mode.text.toString()))
is SendMode.Voice -> renderVoiceMessageMode(mode.text)
}
}
@@ -242,6 +209,14 @@ class MessageComposerFragment : VectorBaseFragment(), A
.onEach { onTypeSelected(it.attachmentType) }
.launchIn(lifecycleScope)
+ messageComposerViewModel.stateFlow.map { it.isFullScreen }
+ .distinctUntilChanged()
+ .onEach { isFullScreen ->
+ val state = if (isFullScreen) ExpandingBottomSheetBehavior.State.Expanded else ExpandingBottomSheetBehavior.State.Collapsed
+ bottomSheetBehavior?.setState(state)
+ }
+ .launchIn(viewLifecycleOwner.lifecycleScope)
+
if (savedInstanceState == null) {
handleShareData()
}
@@ -280,11 +255,45 @@ class MessageComposerFragment : VectorBaseFragment(), A
) { mainState, messageComposerState, attachmentState ->
if (mainState.tombstoneEvent != null) return@withState
- composer.setInvisible(!messageComposerState.isComposerVisible)
+ (composer as? View)?.isInvisible = !messageComposerState.isComposerVisible
composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
(composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled
}
+ private fun setupBottomSheet() {
+ val parentView = view?.parent as? View ?: return
+ bottomSheetBehavior = ExpandingBottomSheetBehavior.from(parentView)?.apply {
+ applyInsetsToContentViewWhenCollapsed = true
+ topOffset = 22
+ useScrimView = true
+ scrimViewTranslationZ = 8
+ minCollapsedHeight = {
+ (composer as? RichTextComposerLayout)?.estimateCollapsedHeight() ?: -1
+ }
+ isDraggable = false
+ callback = object : ExpandingBottomSheetBehavior.Callback {
+ override fun onStateChanged(state: ExpandingBottomSheetBehavior.State) {
+ // Dragging is disabled while the composer is collapsed
+ bottomSheetBehavior?.isDraggable = state != ExpandingBottomSheetBehavior.State.Collapsed
+
+ val setFullScreen = when (state) {
+ ExpandingBottomSheetBehavior.State.Collapsed -> false
+ ExpandingBottomSheetBehavior.State.Expanded -> true
+ else -> return
+ }
+
+ (composer as? RichTextComposerLayout)?.setFullScreen(setFullScreen)
+
+ messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(setFullScreen))
+ }
+
+ override fun onSlidePositionChanged(view: View, yPosition: Float) {
+ (composer as? RichTextComposerLayout)?.notifyIsBeingDragged(yPosition)
+ }
+ }
+ }
+ }
+
private fun setupComposer() {
val composerEditText = composer.editText
composerEditText.setHint(R.string.room_message_placeholder)
@@ -382,8 +391,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
return
}
if (text.isNotBlank()) {
- // We collapse ASAP, if not there will be a slight annoying delay
- composer.collapse(true)
+ composer.renderComposerMode(MessageComposerMode.Normal(""))
lockSendButton = true
if (formattedText != null) {
messageComposerViewModel.handle(MessageComposerAction.SendMessage(text, formattedText, false))
@@ -407,66 +415,12 @@ class MessageComposerFragment : VectorBaseFragment(), A
private fun renderRegularMode(content: CharSequence) {
autoCompleter.exitSpecialMode()
- composer.collapse()
- composer.setTextIfDifferent(content)
- composer.sendButton.contentDescription = getString(R.string.action_send)
+ composer.renderComposerMode(MessageComposerMode.Normal(content))
}
- private fun renderSpecialMode(
- event: TimelineEvent,
- @DrawableRes iconRes: Int,
- @StringRes descriptionRes: Int,
- defaultContent: CharSequence,
- ) {
+ private fun renderSpecialMode(mode: MessageComposerMode.Special) {
autoCompleter.enterSpecialMode()
- // switch to expanded bar
- composer.composerRelatedMessageTitle.apply {
- text = event.senderInfo.disambiguatedDisplayName
- setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(event.root.senderId ?: "@")))
- }
-
- val messageContent: MessageContent? = event.getVectorLastMessageContent()
- val nonFormattedBody = when (messageContent) {
- is MessageAudioContent -> getAudioContentBodyText(messageContent)
- is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
- is MessageBeaconInfoContent -> getString(R.string.live_location_description)
- else -> messageContent?.body.orEmpty()
- }
- var formattedBody: CharSequence? = null
- if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
- val parser = Parser.builder().build()
- val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
- formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor)
- }
- composer.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)
-
- // Image Event
- val data = event.buildImageContentRendererData(dimensionConverter.dpToPx(66))
- val isImageVisible = if (data != null) {
- imageContentRenderer.render(data, ImageContentRenderer.Mode.THUMBNAIL, composer.composerRelatedMessageImage)
- true
- } else {
- imageContentRenderer.clear(composer.composerRelatedMessageImage)
- false
- }
-
- composer.composerRelatedMessageImage.isVisible = isImageVisible
-
- composer.replaceFormattedContent(defaultContent)
-
- composer.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
- composer.sendButton.contentDescription = getString(descriptionRes)
-
- avatarRenderer.render(event.senderInfo.toMatrixItem(), composer.composerRelatedMessageAvatar)
-
- composer.expand {
- if (isAdded) {
- // need to do it here also when not using quick reply
- focusComposerAndShowKeyboard()
- composer.composerRelatedMessageImage.isVisible = isImageVisible
- }
- }
- focusComposerAndShowKeyboard()
+ composer.renderComposerMode(mode)
}
private fun observerUserTyping() {
@@ -489,7 +443,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
}
private fun focusComposerAndShowKeyboard() {
- if (composer.isVisible) {
+ if ((composer as? View)?.isVisible == true) {
composer.editText.showKeyboard(andRequestFocus = true)
}
}
@@ -499,7 +453,7 @@ class MessageComposerFragment : VectorBaseFragment(), A
composer.sendButton.alpha = 0f
composer.sendButton.isVisible = true
composer.sendButton.animate().alpha(1f).setDuration(150).start()
- } else if (!event.isVisible) {
+ } else {
composer.sendButton.isInvisible = true
}
}
@@ -510,15 +464,6 @@ class MessageComposerFragment : VectorBaseFragment(), A
}
}
- private fun getAudioContentBodyText(messageContent: MessageAudioContent): String {
- val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
- return if (messageContent.voiceMessageIndicator != null) {
- getString(R.string.voice_message_reply_content, formattedDuration)
- } else {
- getString(R.string.audio_message_reply_content, messageContent.body, formattedDuration)
- }
- }
-
private fun createEmojiPopup(): EmojiPopup {
return EmojiPopup(
rootView = views.root,
@@ -840,11 +785,6 @@ class MessageComposerFragment : VectorBaseFragment(), A
return displayName
}
- /**
- * Returns the root thread event if we are in a thread room, otherwise returns null.
- */
- fun getRootThreadEventId(): String? = withState(timelineViewModel) { it.rootThreadEventId }
-
/**
* Returns true if the current room is a Thread room, false otherwise.
*/
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt
new file mode 100644
index 0000000000..a401f04bf5
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerMode.kt
@@ -0,0 +1,28 @@
+/*
+ * 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 im.vector.app.features.home.room.detail.composer
+
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+sealed interface MessageComposerMode {
+ data class Normal(val content: CharSequence?) : MessageComposerMode
+
+ sealed class Special(open val event: TimelineEvent, open val defaultContent: CharSequence) : MessageComposerMode
+ data class Edit(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
+ class Quote(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
+ class Reply(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent)
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
index b7e0e29679..44fcf22d4a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt
@@ -19,35 +19,24 @@ package im.vector.app.features.home.room.detail.composer
import android.text.Editable
import android.widget.EditText
import android.widget.ImageButton
-import android.widget.ImageView
-import android.widget.TextView
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 : ComposerEditText.Callback {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
index 939a59fcca..8f4dd9b71d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/PlainTextComposerLayout.kt
@@ -19,44 +19,59 @@ package im.vector.app.features.home.room.detail.composer
import android.content.Context
import android.net.Uri
import android.text.Editable
+import android.text.format.DateUtils
import android.util.AttributeSet
-import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageButton
-import android.widget.ImageView
-import android.widget.TextView
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
+import android.widget.LinearLayout
+import androidx.core.content.ContextCompat
import androidx.core.text.toSpannable
-import androidx.core.view.isInvisible
import androidx.core.view.isVisible
-import androidx.transition.ChangeBounds
-import androidx.transition.Fade
-import androidx.transition.Transition
-import androidx.transition.TransitionManager
-import androidx.transition.TransitionSet
+import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
-import im.vector.app.core.animations.SimpleTransitionListener
+import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.core.extensions.setTextIfDifferent
+import im.vector.app.core.extensions.showKeyboard
+import im.vector.app.core.utils.DimensionConverter
import im.vector.app.databinding.ComposerLayoutBinding
+import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
+import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData
+import im.vector.app.features.html.EventHtmlRenderer
+import im.vector.app.features.html.PillsPostProcessor
+import im.vector.app.features.media.ImageContentRenderer
+import org.commonmark.parser.Parser
+import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
+import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
+import org.matrix.android.sdk.api.util.MatrixItem
+import org.matrix.android.sdk.api.util.toMatrixItem
+import javax.inject.Inject
/**
* Encapsulate the timeline composer UX.
*/
+@AndroidEntryPoint
class PlainTextComposerLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
-) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView {
+) : LinearLayout(context, attrs, defStyleAttr), MessageComposerView {
+
+ @Inject lateinit var avatarRenderer: AvatarRenderer
+ @Inject lateinit var matrixItemColorProvider: MatrixItemColorProvider
+ @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
+ @Inject lateinit var dimensionConverter: DimensionConverter
+ @Inject lateinit var imageContentRenderer: ImageContentRenderer
+ @Inject lateinit var pillsPostProcessorFactory: PillsPostProcessor.Factory
private val views: ComposerLayoutBinding
override var callback: Callback? = null
- private var currentConstraintSetId: Int = -1
-
- private val animationDuration = 100L
-
override val text: Editable?
get() = views.composerEditText.text
@@ -65,37 +80,23 @@ class PlainTextComposerLayout @JvmOverloads constructor(
override val editText: EditText
get() = views.composerEditText
+ @Suppress("RedundantNullableReturnType")
override val emojiButton: ImageButton?
get() = views.composerEmojiButton
override val sendButton: ImageButton
get() = views.sendButton
- override fun setInvisible(isInvisible: Boolean) {
- this.isInvisible = isInvisible
- }
override val attachmentButton: ImageButton
get() = views.attachmentButton
- override val fullScreenButton: ImageButton? = null
- 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 }
init {
inflate(context, R.layout.composer_layout, this)
views = ComposerLayoutBinding.bind(this)
- collapse(false)
+ views.composerEditText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED
+
+ collapse()
views.composerEditText.callback = object : ComposerEditText.Callback {
override fun onRichContentSelected(contentUri: Uri): Boolean {
@@ -121,27 +122,15 @@ class PlainTextComposerLayout @JvmOverloads constructor(
}
}
- override fun replaceFormattedContent(text: CharSequence) {
- setTextIfDifferent(text)
- }
-
- override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) {
- if (currentConstraintSetId == R.layout.composer_layout_constraint_set_compact) {
- // ignore we good
- return
- }
- currentConstraintSetId = R.layout.composer_layout_constraint_set_compact
- applyNewConstraintSet(animate, transitionComplete)
+ private fun collapse(transitionComplete: (() -> Unit)? = null) {
+ views.relatedMessageGroup.isVisible = false
+ transitionComplete?.invoke()
callback?.onExpandOrCompactChange()
}
- override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) {
- if (currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded) {
- // ignore we good
- return
- }
- currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded
- applyNewConstraintSet(animate, transitionComplete)
+ private fun expand(transitionComplete: (() -> Unit)? = null) {
+ views.relatedMessageGroup.isVisible = true
+ transitionComplete?.invoke()
callback?.onExpandOrCompactChange()
}
@@ -149,35 +138,92 @@ class PlainTextComposerLayout @JvmOverloads constructor(
return views.composerEditText.setTextIfDifferent(text)
}
- override fun toggleFullScreen(newValue: Boolean) {
- // Plain text composer has no full screen
+ override fun renderComposerMode(mode: MessageComposerMode) {
+ val specialMode = mode as? MessageComposerMode.Special
+ if (specialMode != null) {
+ renderSpecialMode(specialMode)
+ } else if (mode is MessageComposerMode.Normal) {
+ collapse()
+ editText.setTextIfDifferent(mode.content)
+ }
+
+ views.sendButton.apply {
+ if (mode is MessageComposerMode.Edit) {
+ contentDescription = resources.getString(R.string.action_save)
+ setImageResource(R.drawable.ic_composer_rich_text_save)
+ } else {
+ contentDescription = resources.getString(R.string.action_send)
+ setImageResource(R.drawable.ic_rich_composer_send)
+ }
+ }
}
- private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
- // val wasSendButtonInvisible = views.sendButton.isInvisible
- if (animate) {
- configureAndBeginTransition(transitionComplete)
+ private fun renderSpecialMode(specialMode: MessageComposerMode.Special) {
+ val event = specialMode.event
+ val defaultContent = specialMode.defaultContent
+
+ val iconRes: Int = when (specialMode) {
+ is MessageComposerMode.Reply -> R.drawable.ic_reply
+ is MessageComposerMode.Edit -> R.drawable.ic_edit
+ is MessageComposerMode.Quote -> R.drawable.ic_quote
}
- ConstraintSet().also {
- it.clone(context, currentConstraintSetId)
- it.applyTo(this)
+
+ val pillsPostProcessor = pillsPostProcessorFactory.create(event.roomId)
+
+ // switch to expanded bar
+ views.composerRelatedMessageTitle.apply {
+ text = event.senderInfo.disambiguatedDisplayName
+ setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(event.root.senderId ?: "@")))
+ }
+
+ val messageContent: MessageContent? = event.getVectorLastMessageContent()
+ val nonFormattedBody = when (messageContent) {
+ is MessageAudioContent -> getAudioContentBodyText(messageContent)
+ is MessagePollContent -> messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
+ is MessageBeaconInfoContent -> resources.getString(R.string.live_location_description)
+ else -> messageContent?.body.orEmpty()
+ }
+ var formattedBody: CharSequence? = null
+ if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
+ val parser = Parser.builder().build()
+ val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
+ formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor)
+ }
+ views.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)
+
+ // Image Event
+ val data = event.buildImageContentRendererData(dimensionConverter.dpToPx(66))
+ val isImageVisible = if (data != null) {
+ imageContentRenderer.render(data, ImageContentRenderer.Mode.THUMBNAIL, views.composerRelatedMessageImage)
+ true
+ } else {
+ imageContentRenderer.clear(views.composerRelatedMessageImage)
+ false
+ }
+
+ views.composerRelatedMessageImage.isVisible = isImageVisible
+
+ views.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(context, iconRes))
+
+ avatarRenderer.render(event.senderInfo.toMatrixItem(), views.composerRelatedMessageAvatar)
+
+ views.composerEditText.setText(defaultContent)
+
+ expand {
+ // need to do it here also when not using quick reply
+ if (isVisible) {
+ showKeyboard(andRequestFocus = true)
+ }
+ views.composerRelatedMessageImage.isVisible = isImageVisible
}
- // Might be updated by view state just after, but avoid blinks
- // views.sendButton.isInvisible = wasSendButtonInvisible
}
- private fun configureAndBeginTransition(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()
- }
- })
+ private fun getAudioContentBodyText(messageContent: MessageAudioContent): String {
+ val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
+ return if (messageContent.voiceMessageIndicator != null) {
+ resources.getString(R.string.voice_message_reply_content, formattedDuration)
+ } else {
+ resources.getString(R.string.audio_message_reply_content, messageContent.body, formattedDuration)
}
- TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
index 2d2a4a8cd2..85f163360f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt
@@ -16,25 +16,34 @@
package im.vector.app.features.home.room.detail.composer
+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 im.vector.app.R
-import im.vector.app.core.extensions.animateLayoutChange
+import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setTextIfDifferent
+import im.vector.app.core.extensions.showKeyboard
import im.vector.app.databinding.ComposerRichTextLayoutBinding
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
import io.element.android.wysiwyg.EditorEditText
@@ -46,23 +55,22 @@ 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()
}
@@ -82,37 +90,94 @@ 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(R.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())
+ }
+ }
+
+ 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()
}
@@ -125,11 +190,19 @@ class RichTextComposerLayout @JvmOverloads constructor(
callback?.onAddAttachment()
}
- views.composerFullScreenButton.setOnClickListener {
- callback?.onFullScreenModeChanged()
+ views.composerFullScreenButton.apply {
+ // There's no point in having full screen in landscape since there's almost no vertical space
+ isInvisible = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+ setOnClickListener {
+ callback?.onFullScreenModeChanged()
+ }
}
+ views.composerEditTextOuterBorder.background = borderShapeDrawable
+
setupRichTextMenu()
+
+ updateTextFieldBorder(isFullScreen)
}
private fun setupRichTextMenu() {
@@ -147,6 +220,21 @@ class RichTextComposerLayout @JvmOverloads constructor(
}
}
+ @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()
@@ -197,84 +285,99 @@ class RichTextComposerLayout @JvmOverloads constructor(
button.isSelected = menuState.reversedActions.contains(action)
}
- private fun updateTextFieldBorder() {
- val isExpanded = editText.editableText.lines().count() > 1
- val borderResource = if (isExpanded || isFullScreen) {
- R.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 {
- R.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(R.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(R.string.action_save)
+ setImageResource(R.drawable.ic_composer_rich_text_save)
+ } else {
+ contentDescription = resources.getString(R.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 -> {
+ val senderInfo = mode.event.senderInfo
+ val userName = 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/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
index cd41219371..ca31c53bb3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt
@@ -147,7 +147,8 @@ class VoiceMessageViews(
}
fun showRecordingViews() {
- views.voiceMessageMicButton.setImageResource(R.drawable.ic_voice_mic_recording)
+ views.voiceMessageBackgroundView.isVisible = true
+ views.voiceMessageMicButton.setImageResource(R.drawable.ic_composer_rich_mic_pressed)
views.voiceMessageMicButton.setAttributeTintedBackground(R.drawable.circle_with_halo, R.attr.colorPrimary)
views.voiceMessageMicButton.updateLayoutParams {
setMargins(0, 0, 0, 0)
@@ -172,6 +173,7 @@ class VoiceMessageViews(
fun hideRecordingViews(recordingState: RecordingUiState) {
// We need to animate the lock image first
+ views.voiceMessageBackgroundView.isVisible = false
if (recordingState !is RecordingUiState.Locked) {
views.voiceMessageLockImage.isVisible = false
views.voiceMessageLockImage.animate().translationY(0f).start()
@@ -278,6 +280,7 @@ class VoiceMessageViews(
fun showDraftViews() {
hideRecordingViews(RecordingUiState.Idle)
+ views.voiceMessageBackgroundView.isVisible = true
views.voiceMessageMicButton.isVisible = false
views.voiceMessageSendButton.isVisible = true
views.voiceMessagePlaybackLayout.isVisible = true
@@ -288,6 +291,7 @@ class VoiceMessageViews(
fun showRecordingLockedViews(recordingState: RecordingUiState) {
hideRecordingViews(recordingState)
+ views.voiceMessageBackgroundView.isVisible = true
views.voiceMessagePlaybackLayout.isVisible = true
views.voiceMessagePlaybackTimerIndicator.isVisible = true
views.voicePlaybackControlButton.isVisible = false
diff --git a/vector/src/main/res/drawable/bg_composer_rich_bottom_sheet.xml b/vector/src/main/res/drawable/bg_composer_rich_bottom_sheet.xml
new file mode 100644
index 0000000000..47364373f7
--- /dev/null
+++ b/vector/src/main/res/drawable/bg_composer_rich_bottom_sheet.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/bg_composer_rich_edit_text_expanded.xml b/vector/src/main/res/drawable/bg_composer_rich_edit_text_expanded.xml
deleted file mode 100644
index 26d997e7db..0000000000
--- a/vector/src/main/res/drawable/bg_composer_rich_edit_text_expanded.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/vector/src/main/res/drawable/bg_composer_rich_edit_text_single_line.xml b/vector/src/main/res/drawable/bg_composer_rich_edit_text_single_line.xml
deleted file mode 100644
index 7e2745a137..0000000000
--- a/vector/src/main/res/drawable/bg_composer_rich_edit_text_single_line.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/vector/src/main/res/drawable/bottomsheet_handle.xml b/vector/src/main/res/drawable/bottomsheet_handle.xml
new file mode 100644
index 0000000000..89ccf57ed0
--- /dev/null
+++ b/vector/src/main/res/drawable/bottomsheet_handle.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_composer_collapse.xml b/vector/src/main/res/drawable/ic_composer_collapse.xml
new file mode 100644
index 0000000000..724a833761
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_composer_collapse.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_composer_full_screen.xml b/vector/src/main/res/drawable/ic_composer_full_screen.xml
index 394dc52279..de1862c09b 100644
--- a/vector/src/main/res/drawable/ic_composer_full_screen.xml
+++ b/vector/src/main/res/drawable/ic_composer_full_screen.xml
@@ -1,9 +1,9 @@
-
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+
diff --git a/vector/src/main/res/drawable/ic_composer_rich_mic_pressed.xml b/vector/src/main/res/drawable/ic_composer_rich_mic_pressed.xml
new file mode 100644
index 0000000000..e9dbe610e4
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_composer_rich_mic_pressed.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml b/vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml
new file mode 100644
index 0000000000..c461470de5
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_composer_rich_text_editor_close.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml b/vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml
new file mode 100644
index 0000000000..4556974221
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_composer_rich_text_save.xml b/vector/src/main/res/drawable/ic_composer_rich_text_save.xml
new file mode 100644
index 0000000000..f270d6f8ae
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_composer_rich_text_save.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_rich_composer_add.xml b/vector/src/main/res/drawable/ic_rich_composer_add.xml
new file mode 100644
index 0000000000..3a90a40902
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_rich_composer_add.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_rich_composer_send.xml b/vector/src/main/res/drawable/ic_rich_composer_send.xml
new file mode 100644
index 0000000000..0f99c1670e
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_rich_composer_send.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_voice_mic_recording.xml b/vector/src/main/res/drawable/ic_voice_mic_recording.xml
deleted file mode 100644
index a57852c92f..0000000000
--- a/vector/src/main/res/drawable/ic_voice_mic_recording.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/vector/src/main/res/layout/composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml
index fb0d80278a..7c465891c3 100644
--- a/vector/src/main/res/layout/composer_layout.xml
+++ b/vector/src/main/res/layout/composer_layout.xml
@@ -1,148 +1,210 @@
-
+ android:orientation="vertical">
-
-
-
-
-
-
-
-
-
-
+ android:visibility="gone"
+ tools:visibility="visible">
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
deleted file mode 100644
index 81b978caa6..0000000000
--- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml
+++ /dev/null
@@ -1,197 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml
deleted file mode 100644
index 8cdb388bf9..0000000000
--- a/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml
+++ /dev/null
@@ -1,197 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml
index c5afe1eb44..5f37de2a3a 100644
--- a/vector/src/main/res/layout/composer_rich_text_layout.xml
+++ b/vector/src/main/res/layout/composer_rich_text_layout.xml
@@ -1,183 +1,201 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ 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/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml
deleted file mode 100644
index 1a3023a805..0000000000
--- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_compact.xml
+++ /dev/null
@@ -1,233 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml
deleted file mode 100644
index b0380d2e13..0000000000
--- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_expanded.xml
+++ /dev/null
@@ -1,230 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml b/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml
deleted file mode 100644
index 3105063933..0000000000
--- a/vector/src/main/res/layout/composer_rich_text_layout_constraint_set_fullscreen.xml
+++ /dev/null
@@ -1,234 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vector/src/main/res/layout/fragment_composer.xml b/vector/src/main/res/layout/fragment_composer.xml
index 41c052367a..5038a9e179 100644
--- a/vector/src/main/res/layout/fragment_composer.xml
+++ b/vector/src/main/res/layout/fragment_composer.xml
@@ -4,12 +4,13 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/rootConstraintLayout">
-
+ app:layout_constraintTop_toTopOf="parent" />
-
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"
+ tools:listitem="@layout/item_timeline_event_base" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_timeline_fullscreen.xml b/vector/src/main/res/layout/fragment_timeline_fullscreen.xml
deleted file mode 100644
index 373ca74f56..0000000000
--- a/vector/src/main/res/layout/fragment_timeline_fullscreen.xml
+++ /dev/null
@@ -1,258 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vector/src/main/res/layout/view_rich_text_menu_button.xml b/vector/src/main/res/layout/view_rich_text_menu_button.xml
index 24b19c10b5..b99a29da2b 100644
--- a/vector/src/main/res/layout/view_rich_text_menu_button.xml
+++ b/vector/src/main/res/layout/view_rich_text_menu_button.xml
@@ -2,8 +2,8 @@
+
+
@@ -109,7 +118,7 @@
android:id="@+id/voiceMessageLockImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="28dp"
+ android:layout_marginTop="16dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_voice_message_unlocked"
android:visibility="gone"
@@ -123,7 +132,6 @@
android:id="@+id/voiceMessageLockArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginStart="4dp"
android:layout_marginBottom="14dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_voice_lock_arrow"