Unverified Commit d4d4f52c authored by Filipe de Lima Brito's avatar Filipe de Lima Brito Committed by GitHub

Merge pull request #1254 from RocketChat/new/listen-to-typing-status

[NEW] Listen to typing status
parents c5302609 1f901f3c
...@@ -29,6 +29,8 @@ import chat.rocket.common.model.roomTypeOf ...@@ -29,6 +29,8 @@ import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.setTypingStatus import chat.rocket.core.internal.realtime.setTypingStatus
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.realtime.subscribeTypingStatus
import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.rest.* import chat.rocket.core.internal.rest.*
import chat.rocket.core.model.Command import chat.rocket.core.model.Command
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
...@@ -70,8 +72,11 @@ class ChatRoomPresenter @Inject constructor( ...@@ -70,8 +72,11 @@ class ChatRoomPresenter @Inject constructor(
private var chatRoomId: String? = null private var chatRoomId: String? = null
private var chatRoomType: String? = null private var chatRoomType: String? = null
private val stateChannel = Channel<State>() private val stateChannel = Channel<State>()
private var typingStatusSubscriptionId: String? = null
private var lastState = manager.state private var lastState = manager.state
private var typingStatusList = arrayListOf<String>()
fun setupChatRoom() { fun setupChatRoom() {
launchUI(strategy) { launchUI(strategy) {
...@@ -115,6 +120,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -115,6 +120,7 @@ class ChatRoomPresenter @Inject constructor(
view.hideLoading() view.hideLoading()
} }
subscribeTypingStatus()
if (offset == 0L) { if (offset == 0L) {
subscribeState() subscribeState()
} }
...@@ -326,14 +332,6 @@ class ChatRoomPresenter @Inject constructor( ...@@ -326,14 +332,6 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
fun unsubscribeMessages(chatRoomId: String) {
manager.removeStatusChannel(stateChannel)
manager.unsubscribeRoomMessages(chatRoomId)
// All messages during the subscribed period are assumed to be read,
// and lastSeen is updated as the time when the user leaves the room
markRoomAsRead(chatRoomId)
}
/** /**
* Delete the message with the given id. * Delete the message with the given id.
* *
...@@ -651,6 +649,57 @@ class ChatRoomPresenter @Inject constructor( ...@@ -651,6 +649,57 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
fun disconnect() {
unsubscribeTypingStatus()
if (chatRoomId != null) {
unsubscribeMessages(chatRoomId.toString())
}
}
private suspend fun subscribeTypingStatus() {
client.subscribeTypingStatus(chatRoomId.toString()) { _, id ->
typingStatusSubscriptionId = id
}
for (typingStatus in client.typingStatusChannel) {
processTypingStatus(typingStatus)
}
}
private fun processTypingStatus(typingStatus: Pair<String, Boolean>) {
if (!typingStatusList.any { username -> username == typingStatus.first }) {
if (typingStatus.second) {
typingStatusList.add(typingStatus.first)
}
} else {
typingStatusList.find { username -> username == typingStatus.first }?.let {
typingStatusList.remove(it)
if (typingStatus.second) {
typingStatusList.add(typingStatus.first)
}
}
}
if (typingStatusList.isNotEmpty()) {
view.showTypingStatus(typingStatusList)
} else {
view.hideTypingStatusView()
}
}
private fun unsubscribeTypingStatus() {
typingStatusSubscriptionId?.let {
client.unsubscribe(it)
}
}
private fun unsubscribeMessages(chatRoomId: String) {
manager.removeStatusChannel(stateChannel)
manager.unsubscribeRoomMessages(chatRoomId)
// All messages during the subscribed period are assumed to be read,
// and lastSeen is updated as the time when the user leaves the room
markRoomAsRead(chatRoomId)
}
private fun updateMessage(streamedMessage: Message) { private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) { launchUI(strategy) {
val viewModelStreamedMessage = mapper.map(streamedMessage) val viewModelStreamedMessage = mapper.map(streamedMessage)
...@@ -667,4 +716,4 @@ class ChatRoomPresenter @Inject constructor( ...@@ -667,4 +716,4 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
} }
} }
\ No newline at end of file
...@@ -25,6 +25,18 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -25,6 +25,18 @@ interface ChatRoomView : LoadingView, MessageView {
*/ */
fun sendMessage(text: String) fun sendMessage(text: String)
/**
* Shows the username(s) of the user(s) who is/are typing in the chat room.
*
* @param usernameList The list of username to show.
*/
fun showTypingStatus(usernameList: ArrayList<String>)
/**
* Hides the typing status view.
*/
fun hideTypingStatusView()
/** /**
* Perform file selection with the mime type [filter] * Perform file selection with the mime type [filter]
*/ */
......
...@@ -13,7 +13,10 @@ import android.support.v4.app.Fragment ...@@ -13,7 +13,10 @@ import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.text.SpannableStringBuilder
import android.view.* import android.view.*
import androidx.core.text.bold
import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.* import chat.rocket.android.chatroom.adapter.*
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
...@@ -94,8 +97,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -94,8 +97,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private var playComposeMessageButtonsAnimation = true private var playComposeMessageButtonsAnimation = true
// For reveal and unreveal anim. // For reveal and unreveal anim.
private val hypotenuse by lazy { Math.hypot(root_layout.width.toDouble(), root_layout.height.toDouble()).toFloat() } private val hypotenuse by lazy {
private val max by lazy { Math.max(layout_message_attachment_options.width.toDouble(), layout_message_attachment_options.height.toDouble()).toFloat() } Math.hypot(
root_layout.width.toDouble(),
root_layout.height.toDouble()
).toFloat()
}
private val max by lazy {
Math.max(
layout_message_attachment_options.width.toDouble(),
layout_message_attachment_options.height.toDouble()
).toFloat()
}
private val centerX by lazy { recycler_view.right } private val centerX by lazy { recycler_view.right }
private val centerY by lazy { recycler_view.bottom } private val centerY by lazy { recycler_view.bottom }
private val handler = Handler() private val handler = Handler()
...@@ -155,7 +168,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -155,7 +168,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
recycler_view.removeOnScrollListener(onScrollListener) recycler_view.removeOnScrollListener(onScrollListener)
recycler_view.removeOnLayoutChangeListener(layoutChangeListener) recycler_view.removeOnLayoutChangeListener(layoutChangeListener)
presenter.unsubscribeMessages(chatRoomId) presenter.disconnect()
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
unsubscribeComposeTextMessage() unsubscribeComposeTextMessage()
...@@ -319,6 +332,37 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -319,6 +332,37 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun showTypingStatus(usernameList: ArrayList<String>) {
ui {
when (usernameList.size) {
1 -> {
text_typing_status.text =
SpannableStringBuilder()
.bold { append(usernameList[0]) }
.append(getString(R.string.msg_is_typing))
}
2 -> {
text_typing_status.text =
SpannableStringBuilder()
.bold { append(usernameList[0]) }
.append(getString(R.string.msg_and))
.bold { append(usernameList[1]) }
.append(getString(R.string.msg_are_typing))
}
else -> {
text_typing_status.text = getString(R.string.msg_several_users_are_typing)
}
}
text_typing_status.isVisible = true
}
}
override fun hideTypingStatusView() {
ui {
text_typing_status.isVisible = false
}
}
override fun uploadFile(uri: Uri) { override fun uploadFile(uri: Uri) {
// TODO Just leaving a blank message that comes with the file for now. In the future lets add the possibility to add a message with the file to be uploaded. // TODO Just leaving a blank message that comes with the file for now. In the future lets add the possibility to add a message with the file to be uploaded.
presenter.uploadFile(chatRoomId, uri, "") presenter.uploadFile(chatRoomId, uri, "")
......
...@@ -14,6 +14,7 @@ import android.support.v7.widget.SearchView ...@@ -14,6 +14,7 @@ import android.support.v7.widget.SearchView
import android.view.* import android.view.*
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.RadioGroup import android.widget.RadioGroup
import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView import chat.rocket.android.chatrooms.presentation.ChatRoomsView
...@@ -166,11 +167,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -166,11 +167,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
/*val diff = async(CommonPool) { /*val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet)) DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet))
}.await()*/ }.await()*/
if (newDataSet.isEmpty()) { text_no_search.isVisible = newDataSet.isEmpty()
text_no_search.visibility = View.VISIBLE
}else{
text_no_search.visibility = View.GONE
}
if (isActive) { if (isActive) {
adapter.baseAdapter.updateRooms(newDataSet) adapter.baseAdapter.updateRooms(newDataSet)
// TODO - fix crash to re-enable diff.dispatchUpdatesTo(adapter) // TODO - fix crash to re-enable diff.dispatchUpdatesTo(adapter)
......
...@@ -110,7 +110,6 @@ class ConnectionManager(internal val client: RocketChatClient) { ...@@ -110,7 +110,6 @@ class ConnectionManager(internal val client: RocketChatClient) {
Timber.d("Received new Message for room ${message.roomId}") Timber.d("Received new Message for room ${message.roomId}")
val channel = roomMessagesChannels[message.roomId] val channel = roomMessagesChannels[message.roomId]
channel?.send(message) channel?.send(message)
} }
} }
...@@ -131,6 +130,7 @@ class ConnectionManager(internal val client: RocketChatClient) { ...@@ -131,6 +130,7 @@ class ConnectionManager(internal val client: RocketChatClient) {
} }
} }
} }
client.connect() client.connect()
// Broadcast initial state... // Broadcast initial state...
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout" android:id="@+id/root_layout"
...@@ -12,23 +11,23 @@ ...@@ -12,23 +11,23 @@
android:id="@+id/view_loading" android:id="@+id/view_loading"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone" android:visibility="gone"
app:indicatorColor="@color/black" app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator" app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" /> tools:visibility="visible" />
<FrameLayout <FrameLayout
android:id="@+id/message_list_container" android:id="@+id/message_list_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toTopOf="@id/text_typing_status"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"> app:layout_constraintTop_toTopOf="parent">
<include <include
android:id="@+id/layout_message_list" android:id="@+id/layout_message_list"
...@@ -44,52 +43,65 @@ ...@@ -44,52 +43,65 @@
android:layout_height="100dp" android:layout_height="100dp"
android:src="@drawable/ic_chat_black_24dp" android:src="@drawable/ic_chat_black_24dp"
android:tint="@color/icon_grey" android:tint="@color/icon_grey"
app:layout_constraintStart_toStartOf="parent" android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/text_chat_title"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_chat_title"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <TextView
android:id="@+id/text_chat_title" android:id="@+id/text_chat_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/msg_no_chat_title" android:text="@string/msg_no_chat_title"
app:layout_constraintStart_toStartOf="parent" android:textColor="@color/colorSecondaryText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/image_chat_icon"
app:layout_constraintBottom_toTopOf="@id/text_chat_description"
android:textSize="20sp" android:textSize="20sp"
android:layout_marginTop="24dp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="@color/colorSecondaryText"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"/> app:layout_constraintBottom_toTopOf="@id/text_chat_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/image_chat_icon"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/text_chat_description" android:id="@+id/text_chat_description"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/msg_no_chat_description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_chat_title"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/msg_no_chat_description"
android:textAlignment="center" android:textAlignment="center"
android:textSize="16sp"
android:textColor="@color/colorSecondaryTextLight" android:textColor="@color/colorSecondaryTextLight"
android:textSize="16sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"/> app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_chat_title"
tools:visibility="visible" />
<chat.rocket.android.widget.autocompletion.ui.SuggestionsView <chat.rocket.android.widget.autocompletion.ui.SuggestionsView
android:id="@+id/suggestions_view" android:id="@+id/suggestions_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/suggestion_background_color"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer" />
<TextView
android:id="@+id/text_typing_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:maxLines="2"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer" app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:background="@color/suggestion_background_color" /> app:layout_constraintEnd_toStartOf="parent" />
<include <include
android:id="@+id/layout_message_composer" android:id="@+id/layout_message_composer"
...@@ -102,18 +114,18 @@ ...@@ -102,18 +114,18 @@
android:id="@+id/view_dim" android:id="@+id/view_dim"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:background="@color/colorDim" android:background="@color/colorDim"
android:visibility="gone" /> android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer" />
<include <include
android:id="@+id/layout_message_attachment_options" android:id="@+id/layout_message_attachment_options"
layout="@layout/message_attachment_options" layout="@layout/message_attachment_options"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:layout_margin="5dp" android:layout_margin="5dp"
android:visibility="gone" /> android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer" />
<TextView <TextView
android:id="@+id/connection_status_text" android:id="@+id/connection_status_text"
......
...@@ -111,6 +111,14 @@ ...@@ -111,6 +111,14 @@
<string name="msg_no_chat_title">Sin mensajes de chat</string> <string name="msg_no_chat_title">Sin mensajes de chat</string>
<string name="msg_no_chat_description">Comience a conversar para ver\nsus mensajes aquí.</string> <string name="msg_no_chat_description">Comience a conversar para ver\nsus mensajes aquí.</string>
<string name="msg_edited">(editado)</string> <string name="msg_edited">(editado)</string>
// TODO: Add proper translation.
<string name="msg_and">\u0020and\u0020</string>
// TODO: Add proper translation.
<string name="msg_is_typing">\u0020is typing…</string>
// TODO: Add proper translation.
<string name="msg_are_typing">\u0020are typing…</string>
// TODO: Add proper translation.
<string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">No se han encontrado resultados</string> <string name="msg_no_search_found">No se han encontrado resultados</string>
<!-- System messages --> <!-- System messages -->
......
...@@ -111,6 +111,14 @@ ...@@ -111,6 +111,14 @@
<string name="msg_no_chat_title">Aucun message de discussion</string> <string name="msg_no_chat_title">Aucun message de discussion</string>
<string name="msg_no_chat_description">Commencez à converser pour voir\nvos messages ici.</string> <string name="msg_no_chat_description">Commencez à converser pour voir\nvos messages ici.</string>
<string name="msg_edited">(édité)</string> <string name="msg_edited">(édité)</string>
// TODO: Add proper translation.
<string name="msg_and">\u0020and\u0020</string>
// TODO: Add proper translation.
<string name="msg_is_typing">\u0020is typing…</string>
// TODO: Add proper translation.
<string name="msg_are_typing">\u0020are typing…</string>
// TODO: Add proper translation.
<string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">Aucun résultat trouvé</string> <string name="msg_no_search_found">Aucun résultat trouvé</string>
<!-- System messages --> <!-- System messages -->
......
...@@ -113,6 +113,14 @@ ...@@ -113,6 +113,14 @@
<string name="msg_no_chat_title">कोई चैट संदेश नहीं</string> <string name="msg_no_chat_title">कोई चैट संदेश नहीं</string>
<string name="msg_no_chat_description">यहां अपने संदेश देखने के लिए\nबातचीत शुरू करें।</string> <string name="msg_no_chat_description">यहां अपने संदेश देखने के लिए\nबातचीत शुरू करें।</string>
<string name="msg_edited">(संपादित)</string> <string name="msg_edited">(संपादित)</string>
// TODO: Add proper translation.
<string name="msg_and">\u0020and\u0020</string>
// TODO: Add proper translation.
<string name="msg_is_typing">\u0020is typing…</string>
// TODO: Add proper translation.
<string name="msg_are_typing">\u0020are typing…</string>
// TODO: Add proper translation.
<string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">कोई परिणाम नहीं मिला</string> <string name="msg_no_search_found">कोई परिणाम नहीं मिला</string>
<!-- System messages --> <!-- System messages -->
......
...@@ -107,6 +107,10 @@ ...@@ -107,6 +107,10 @@
<string name="msg_image_saved_successfully">Imagem salva na galeria</string> <string name="msg_image_saved_successfully">Imagem salva na galeria</string>
<string name="msg_image_saved_failed">Falha ao salvar a imagem</string> <string name="msg_image_saved_failed">Falha ao salvar a imagem</string>
<string name="msg_edited">(editado)</string> <string name="msg_edited">(editado)</string>
<string name="msg_and">\u0020e\u0020</string>
<string name="msg_is_typing">\u0020está digitando…</string>
<string name="msg_are_typing">\u0020estão digitando…</string>
<string name="msg_several_users_are_typing">Vários usuários estão digitando…</string>
<string name="msg_no_search_found">nenhum resultado encontrado</string> <string name="msg_no_search_found">nenhum resultado encontrado</string>
<!-- System messages --> <!-- System messages -->
......
...@@ -108,6 +108,10 @@ ...@@ -108,6 +108,10 @@
<string name="msg_image_saved_successfully">Image has been saved to gallery</string> <string name="msg_image_saved_successfully">Image has been saved to gallery</string>
<string name="msg_image_saved_failed">Failed to save image</string> <string name="msg_image_saved_failed">Failed to save image</string>
<string name="msg_edited">(edited)</string> <string name="msg_edited">(edited)</string>
<string name="msg_and">\u0020and\u0020</string>
<string name="msg_is_typing">\u0020is typing…</string>
<string name="msg_are_typing">\u0020are typing…</string>
<string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">No result found</string> <string name="msg_no_search_found">No result found</string>
<!-- System messages --> <!-- System messages -->
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment