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 & %2$s are typing…</string> <string name="room_many_users_are_typing">%1$s & %2$s & 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>