From da9fdf1b1847b63c86fad4edc2d08171506b4a07 Mon Sep 17 00:00:00 2001
From: Ahmed Radhouane Belkilani <ahmed-radhouane.belkilani@niji.fr>
Date: Fri, 11 Feb 2022 10:18:13 +0100
Subject: [PATCH] #3296

 - Adding a typing message notification view at the bottom of the timeline in rooms.

Signed-off-by: Ahmed Radhouane Belkilani <arbelkilani@gmail.com>
---
 ...ges_before_rebase__Default_Changelist_.xml |  4 +
 changelog.d/3296.bugfix                       |  1 +
 .../sdk/api/session/room/sender/SenderInfo.kt |  3 +
 .../app/core/ui/views/TypingMessageAvatar.kt  | 60 ++++++++++++
 .../core/ui/views/TypingMessageDotsView.kt    | 92 +++++++++++++++++++
 .../app/core/ui/views/TypingMessageView.kt    | 55 +++++++++++
 .../home/room/detail/RoomDetailViewState.kt   |  4 +-
 .../home/room/detail/TimelineFragment.kt      | 15 +++
 .../home/room/detail/TimelineViewModel.kt     |  1 +
 .../features/home/room/typing/TypingHelper.kt | 11 +++
 .../src/main/res/drawable/ic_typing_dot.xml   |  9 ++
 .../src/main/res/layout/fragment_timeline.xml | 21 +++--
 .../main/res/layout/typing_message_layout.xml | 38 ++++++++
 .../layout/vector_settings_round_avatar.xml   |  4 +-
 .../layout/view_typing_message_avatars.xml    |  7 ++
 vector/src/main/res/values/strings.xml        |  3 +
 16 files changed, 319 insertions(+), 9 deletions(-)
 create mode 100644 .idea/shelf/Uncommitted_changes_before_rebase__Default_Changelist_.xml
 create mode 100644 changelog.d/3296.bugfix
 create mode 100644 vector/src/main/java/im/vector/app/core/ui/views/TypingMessageAvatar.kt
 create mode 100644 vector/src/main/java/im/vector/app/core/ui/views/TypingMessageDotsView.kt
 create mode 100644 vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt
 create mode 100644 vector/src/main/res/drawable/ic_typing_dot.xml
 create mode 100644 vector/src/main/res/layout/typing_message_layout.xml
 create mode 100644 vector/src/main/res/layout/view_typing_message_avatars.xml

diff --git a/.idea/shelf/Uncommitted_changes_before_rebase__Default_Changelist_.xml b/.idea/shelf/Uncommitted_changes_before_rebase__Default_Changelist_.xml
new file mode 100644
index 00000000000..341c0878583
--- /dev/null
+++ b/.idea/shelf/Uncommitted_changes_before_rebase__Default_Changelist_.xml
@@ -0,0 +1,4 @@
+<changelist name="Uncommitted_changes_before_rebase_[Default_Changelist]" date="1644570474870" recycled="true" deleted="true">
+  <option name="PATH" value="$PROJECT_DIR$/.idea/shelf/Uncommitted_changes_before_rebase_[Default_Changelist]/shelved.patch" />
+  <option name="DESCRIPTION" value="Uncommitted changes before rebase [Default Changelist]" />
+</changelist>
\ No newline at end of file
diff --git a/changelog.d/3296.bugfix b/changelog.d/3296.bugfix
new file mode 100644
index 00000000000..e5f8799f214
--- /dev/null
+++ b/changelog.d/3296.bugfix
@@ -0,0 +1 @@
+Typing notifications moved from the header to the bottom of the timeline.
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt
index 9b73136fc37..f739fe9e1b3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/sender/SenderInfo.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.api.session.room.sender
 
+import org.matrix.android.sdk.api.util.MatrixItem
 import org.matrix.android.sdk.internal.util.replaceSpaceChars
 
 data class SenderInfo(
@@ -35,3 +36,5 @@ data class SenderInfo(
             else                                      -> "$displayName ($userId)"
         }
 }
+
+fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageAvatar.kt b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageAvatar.kt
new file mode 100644
index 00000000000..2682a97a2c7
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageAvatar.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.core.ui.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import im.vector.app.core.utils.DimensionConverter
+import im.vector.app.features.home.AvatarRenderer
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+import org.matrix.android.sdk.api.util.toMatrixItem
+
+class TypingMessageAvatar @JvmOverloads constructor(
+        context: Context,
+        attrs: AttributeSet? = null,
+        defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr) {
+
+    companion object {
+        const val AVATAR_SIZE_DP = 24
+        const val OVERLAP_FACT0R = -3 // =~ 30% to left
+    }
+
+    fun render(typingUsers: List<SenderInfo>, avatarRender: AvatarRenderer) {
+        removeAllViews()
+        for ((index, value) in typingUsers.withIndex()) {
+            val avatar = ImageView(context)
+            avatar.id = View.generateViewId()
+            val layoutParams = MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+            if (index != 0) layoutParams.marginStart = DimensionConverter(resources).dpToPx(AVATAR_SIZE_DP / OVERLAP_FACT0R)
+            layoutParams.width = DimensionConverter(resources).dpToPx(AVATAR_SIZE_DP)
+            layoutParams.height = DimensionConverter(resources).dpToPx(AVATAR_SIZE_DP)
+            avatar.layoutParams = layoutParams
+            avatarRender.render(value.toMatrixItem(), avatar)
+            addView(avatar)
+        }
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        removeAllViews()
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageDotsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageDotsView.kt
new file mode 100644
index 00000000000..66282347041
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageDotsView.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.core.ui.views
+
+import android.animation.ValueAnimator
+import android.content.Context
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.annotation.DrawableRes
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.view.setMargins
+import im.vector.app.R
+
+class TypingMessageDotsView(context: Context, attrs: AttributeSet) :
+    LinearLayout(context, attrs) {
+
+    companion object {
+        const val DEFAULT_CIRCLE_DURATION = 1000L
+        const val DEFAULT_START_ANIM_CIRCLE_DURATION = 300L
+        const val DEFAULT_MAX_ALPHA = 1f
+        const val DEFAULT_MIN_ALPHA = .5f
+        const val DEFAULT_DOTS_MARGIN = 5
+        const val DEFAULT_DOTS_COUNT = 3
+    }
+
+    private val circles = mutableListOf<View>()
+
+    init {
+        orientation = HORIZONTAL
+        gravity = Gravity.CENTER
+        setCircles()
+    }
+
+    private fun setCircles() {
+        circles.clear()
+        removeAllViews()
+        for (i in 0 until DEFAULT_DOTS_COUNT) {
+            val view = obtainCircle(R.drawable.ic_typing_dot)
+            addView(view)
+            circles.add(view)
+        }
+    }
+
+    private fun obtainCircle(@DrawableRes imageCircle: Int): View {
+        val image = AppCompatImageView(context)
+        image.id = View.generateViewId()
+        val params = MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+        params.setMargins(DEFAULT_DOTS_MARGIN)
+        image.layoutParams = params
+        image.setImageResource(imageCircle)
+        image.adjustViewBounds = false
+        return image
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        circles.forEachIndexed { index, circle -> animateCircle(index, circle) }
+    }
+
+    private fun animateCircle(index: Int, circle: View) {
+        val animator = ValueAnimator.ofFloat(DEFAULT_MAX_ALPHA, DEFAULT_MIN_ALPHA)
+        animator.duration = DEFAULT_CIRCLE_DURATION
+        animator.startDelay = DEFAULT_START_ANIM_CIRCLE_DURATION * index
+        animator.repeatCount = ValueAnimator.INFINITE
+        animator.addUpdateListener {
+            circle.alpha = it.animatedValue as Float
+        }
+        animator.start()
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        circles.forEach { it.clearAnimation() }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt
new file mode 100644
index 00000000000..11248bde74c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.core.ui.views
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.constraintlayout.widget.ConstraintLayout
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.databinding.TypingMessageLayoutBinding
+import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.home.room.typing.TypingHelper
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class TypingMessageView @JvmOverloads constructor(
+        context: Context,
+        attrs: AttributeSet? = null,
+        defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+    val views: TypingMessageLayoutBinding
+
+    @Inject
+    lateinit var typingHelper: TypingHelper
+
+    init {
+        inflate(context, R.layout.typing_message_layout, this)
+        views = TypingMessageLayoutBinding.bind(this)
+    }
+
+    fun render(typingUsers: List<SenderInfo>, avatarRender: AvatarRenderer) {
+        views.usersName.text = typingHelper.getNotificationTypingMessage(typingUsers)
+        views.avatars.render(typingUsers, avatarRender)
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        removeAllViews()
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
index 71a299e11bf..e2b97b09001 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.initsync.SyncStatusService
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
 import org.matrix.android.sdk.api.session.sync.SyncState
 import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState
 import org.matrix.android.sdk.api.session.widgets.model.Widget
@@ -72,7 +73,8 @@ data class RoomDetailViewState(
         val jitsiState: JitsiState = JitsiState(),
         val switchToParentSpace: Boolean = false,
         val rootThreadEventId: String? = null,
-        val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState()
+        val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(),
+        val typingUsers: List<SenderInfo>? = null
 ) : MavericksState {
 
     constructor(args: TimelineArgs) : this(
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 b6cbd538f32..8fbe8e9afaa 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
@@ -273,6 +273,7 @@ class TimelineFragment @Inject constructor(
         CurrentCallsView.Callback {
 
     companion object {
+
         /**
          * Sanitize the display name.
          *
@@ -287,6 +288,7 @@ class TimelineFragment @Inject constructor(
             return displayName
         }
 
+        const val MAX_TYPING_MESSAGE_USERS_COUNT = 4
         private const val ircPattern = " (IRC)"
     }
 
@@ -1546,6 +1548,7 @@ class TimelineFragment @Inject constructor(
         invalidateOptionsMenu()
         val summary = mainState.asyncRoomSummary()
         renderToolbar(summary, mainState.formattedTypingUsers)
+        renderTypingMessageNotification(summary, mainState)
         views.removeJitsiWidgetView.render(mainState)
         if (mainState.hasFailedSending) {
             lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true
@@ -1558,6 +1561,7 @@ class TimelineFragment @Inject constructor(
             views.jumpToBottomView.drawBadge = summary.hasUnreadMessages
             timelineEventController.update(mainState)
             lazyLoadedViews.inviteView(false)?.isVisible = false
+
             if (mainState.tombstoneEvent == null) {
                 views.composerLayout.isInvisible = !messageComposerState.isComposerVisible
                 views.voiceMessageRecorderView.isVisible = messageComposerState.isVoiceMessageRecorderVisible
@@ -1601,6 +1605,17 @@ class TimelineFragment @Inject constructor(
         voiceMessageRecorderView.isVisible = false
     }
 
+    private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) {
+        if (!isThreadTimeLine() && roomSummary != null) {
+            views.typingMessageView.isInvisible = state.typingUsers.isNullOrEmpty()
+            state.typingUsers?.let { senders ->
+                views.typingMessageView.render(senders.take(MAX_TYPING_MESSAGE_USERS_COUNT), avatarRenderer)
+            }
+        } else {
+            views.typingMessageView.isInvisible = true
+        }
+    }
+
     private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) {
         if (!isThreadTimeLine()) {
             views.includeRoomToolbar.roomToolbarContentView.isVisible = true
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 0198c77280f..14f5df90559 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -1172,6 +1172,7 @@ class TimelineViewModel @AssistedInject constructor(
             setState {
                 val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
                 copy(
+                        typingUsers = summary.typingUsers,
                         formattedTypingUsers = typingMessage,
                         hasFailedSending = summary.hasFailedSending
                 )
diff --git a/vector/src/main/java/im/vector/app/features/home/room/typing/TypingHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/typing/TypingHelper.kt
index 5878f99468f..ca948e6fdb1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/typing/TypingHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/typing/TypingHelper.kt
@@ -42,4 +42,15 @@ class TypingHelper @Inject constructor(private val stringProvider: StringProvide
                         typingUsers[1].disambiguatedDisplayName)
         }
     }
+
+    fun getNotificationTypingMessage(typingUsers: List<SenderInfo>): String {
+        return when {
+            typingUsers.isEmpty() -> ""
+            typingUsers.size == 1 -> typingUsers[0].disambiguatedDisplayName
+            typingUsers.size == 2 -> stringProvider.getString(R.string.room_notification_two_users_are_typing,
+                    typingUsers[0].disambiguatedDisplayName, typingUsers[1].disambiguatedDisplayName)
+            else                  -> stringProvider.getString(R.string.room_notification_more_than_two_users_are_typing,
+                    typingUsers[0].disambiguatedDisplayName, typingUsers[1].disambiguatedDisplayName)
+        }
+    }
 }
diff --git a/vector/src/main/res/drawable/ic_typing_dot.xml b/vector/src/main/res/drawable/ic_typing_dot.xml
new file mode 100644
index 00000000000..0a3c10a0c93
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_typing_dot.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="7dp"
+    android:height="6dp"
+    android:viewportWidth="7"
+    android:viewportHeight="6">
+  <path
+      android:pathData="M3.22495,3.00004m-2.9,0a2.9,2.9 0,1 1,5.8 0a2.9,2.9 0,1 1,-5.8 0"
+      android:fillColor="#8D99A5"/>
+</vector>
diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml
index cb8e984cc69..137791d13d0 100644
--- a/vector/src/main/res/layout/fragment_timeline.xml
+++ b/vector/src/main/res/layout/fragment_timeline.xml
@@ -17,8 +17,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:minHeight="48dp"
-            android:visibility="gone"
-            tools:visibility="gone" />
+            android:visibility="gone"/>
 
         <com.google.android.material.appbar.MaterialToolbar
             android:id="@+id/roomToolbar"
@@ -87,8 +86,19 @@
         app:closeIcon="@drawable/ic_close_24dp"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"
-        tools:visibility="visible" />
+        app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"/>
+
+    <im.vector.app.core.ui.views.TypingMessageView
+        android:id="@+id/typingMessageView"
+        app:layout_constraintBottom_toTopOf="@id/composerLayout"
+        app:layout_constraintTop_toBottomOf="@id/timelineRecyclerView"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        android:layout_width="0dp"
+        android:paddingStart="20dp"
+        android:paddingEnd="20dp"
+        android:visibility="invisible"
+        android:layout_height="wrap_content"/>
 
     <im.vector.app.core.ui.views.NotificationAreaView
         android:id="@+id/notificationAreaView"
@@ -119,8 +129,7 @@
         android:visibility="gone"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        tools:visibility="visible" />
+        app:layout_constraintStart_toStartOf="parent"/>
 
     <im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
         android:id="@+id/voiceMessageRecorderView"
diff --git a/vector/src/main/res/layout/typing_message_layout.xml b/vector/src/main/res/layout/typing_message_layout.xml
new file mode 100644
index 00000000000..c8b334c628b
--- /dev/null
+++ b/vector/src/main/res/layout/typing_message_layout.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <im.vector.app.core.ui.views.TypingMessageAvatar
+        android:id="@+id/avatars"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/users_name"
+        style="@style/Widget.Vector.TextView.Body"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:layout_marginStart="8dp"
+        android:gravity="center"
+        android:textColor="?vctr_content_secondary"
+        app:layout_constraintBottom_toBottomOf="@id/avatars"
+        app:layout_constraintStart_toEndOf="@id/avatars"
+        app:layout_constraintTop_toTopOf="@id/avatars" />
+
+    <im.vector.app.core.ui.views.TypingMessageDotsView
+        android:id="@+id/viewDots"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:layout_marginStart="8dp"
+        android:gravity="center"
+        android:visibility="visible"
+        app:layout_constraintBottom_toBottomOf="@id/users_name"
+        app:layout_constraintStart_toEndOf="@id/users_name"
+        app:layout_constraintTop_toTopOf="@id/users_name" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/vector/src/main/res/layout/vector_settings_round_avatar.xml b/vector/src/main/res/layout/vector_settings_round_avatar.xml
index 596eef5d3c5..ca9c39825f6 100644
--- a/vector/src/main/res/layout/vector_settings_round_avatar.xml
+++ b/vector/src/main/res/layout/vector_settings_round_avatar.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="40dp"
-    android:layout_height="40dp">
+    android:layout_width="24dp"
+    android:layout_height="24dp">
 
     <ImageView
         android:id="@+id/settings_avatar"
diff --git a/vector/src/main/res/layout/view_typing_message_avatars.xml b/vector/src/main/res/layout/view_typing_message_avatars.xml
new file mode 100644
index 00000000000..95363ae3255
--- /dev/null
+++ b/vector/src/main/res/layout/view_typing_message_avatars.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:gravity="center"
+    android:layout_width="wrap_content"
+    android:layout_height="16dp"/>
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 800d1092f82..227527d9fa8 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -956,6 +956,9 @@
     <string name="room_one_user_is_typing">%s is typing…</string>
     <string name="room_two_users_are_typing">%1$s &#038; %2$s are typing…</string>
     <string name="room_many_users_are_typing">%1$s &#038; %2$s &#038; others are typing…</string>
+    <!--TODO #3296 add next two strings values -->
+    <string name="room_notification_two_users_are_typing">%1$s and %2$s</string>
+    <string name="room_notification_more_than_two_users_are_typing">%1$s, %2$s and others</string>
     <string name="room_message_placeholder_encrypted">Send an encrypted message…</string>
     <string name="room_message_placeholder_not_encrypted">Send a message (unencrypted)…</string>
     <string name="room_message_placeholder_reply_to_encrypted">Send an encrypted reply…</string>