Commit 77159deb authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Merge branch 'develop' of github.com:RocketChat/Rocket.Chat.Android into...

Merge branch 'develop' of github.com:RocketChat/Rocket.Chat.Android into improvement/draw-and-app-modularization
parents 86830af9 33d605bf
...@@ -16,6 +16,12 @@ android { ...@@ -16,6 +16,12 @@ android {
versionName "2.4.0" versionName "2.4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
} }
signingConfigs { signingConfigs {
......
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "06359a8c2943365dd094bc5dff210203",
"entities": [
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `username` TEXT, `name` TEXT, `status` TEXT NOT NULL, `utcOffset` REAL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "utcOffset",
"columnName": "utcOffset",
"affinity": "REAL",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_users_username",
"unique": false,
"columnNames": [
"username"
],
"createSql": "CREATE INDEX `index_users_username` ON `${TABLE_NAME}` (`username`)"
}
],
"foreignKeys": []
},
{
"tableName": "chatrooms",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` TEXT NOT NULL, `type` TEXT NOT NULL, `name` TEXT NOT NULL, `fullname` TEXT, `userId` TEXT, `ownerId` TEXT, `readonly` INTEGER, `isDefault` INTEGER, `favorite` INTEGER, `open` INTEGER NOT NULL, `alert` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `userMentions` INTEGER, `groupMentions` INTEGER, `updatedAt` INTEGER, `timestamp` INTEGER, `lastSeen` INTEGER, `lastMessageText` TEXT, `lastMessageUserId` TEXT, `lastMessageTimestamp` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`ownerId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`userId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`lastMessageUserId`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "subscriptionId",
"columnName": "subscriptionId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fullname",
"columnName": "fullname",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "readonly",
"columnName": "readonly",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "isDefault",
"columnName": "isDefault",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "favorite",
"columnName": "favorite",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "open",
"columnName": "open",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alert",
"columnName": "alert",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userMentions",
"columnName": "userMentions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "groupMentions",
"columnName": "groupMentions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastMessageText",
"columnName": "lastMessageText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessageUserId",
"columnName": "lastMessageUserId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastMessageTimestamp",
"columnName": "lastMessageTimestamp",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_chatrooms_userId",
"unique": false,
"columnNames": [
"userId"
],
"createSql": "CREATE INDEX `index_chatrooms_userId` ON `${TABLE_NAME}` (`userId`)"
},
{
"name": "index_chatrooms_ownerId",
"unique": false,
"columnNames": [
"ownerId"
],
"createSql": "CREATE INDEX `index_chatrooms_ownerId` ON `${TABLE_NAME}` (`ownerId`)"
},
{
"name": "index_chatrooms_subscriptionId",
"unique": true,
"columnNames": [
"subscriptionId"
],
"createSql": "CREATE UNIQUE INDEX `index_chatrooms_subscriptionId` ON `${TABLE_NAME}` (`subscriptionId`)"
},
{
"name": "index_chatrooms_updatedAt",
"unique": false,
"columnNames": [
"updatedAt"
],
"createSql": "CREATE INDEX `index_chatrooms_updatedAt` ON `${TABLE_NAME}` (`updatedAt`)"
}
],
"foreignKeys": [
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"ownerId"
],
"referencedColumns": [
"id"
]
},
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"userId"
],
"referencedColumns": [
"id"
]
},
{
"table": "users",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"lastMessageUserId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"06359a8c2943365dd094bc5dff210203\")"
]
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.presentation package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.chatroom.uimodel.BaseUiModel import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
......
package chat.rocket.android.chatroom.ui
import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.core.view.isVisible
import chat.rocket.android.util.extensions.getFileName
import chat.rocket.android.util.extensions.getMimeType
fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
activity?.let { fragmentActivity ->
uri.getMimeType(fragmentActivity).let { mimeType ->
when {
mimeType.startsWith("image") -> {
imagePreview.isVisible = true
imagePreview.setImageURI(uri)
}
mimeType.startsWith("video") -> {
audioVideoAttachment.isVisible = true
}
else -> {
textFile.isVisible = true
textFile.text = uri.getFileName(fragmentActivity)
}
}
}
}
sendButton.setOnClickListener {
presenter.uploadFile(chatRoomId, uri, (citation ?: "") + description.text.toString())
clearMessageComposition()
alertDialog.dismiss()
}
cancelButton.setOnClickListener { alertDialog.dismiss() }
alertDialog.show()
}
fun ChatRoomFragment.showDrawAttachmentDialog(byteArray: ByteArray) {
imagePreview.isVisible = true
imagePreview.setImageDrawable(Drawable.createFromStream(byteArray.inputStream(), ""))
sendButton.setOnClickListener {
presenter.uploadDrawingImage(
chatRoomId,
byteArray,
(citation ?: "") + description.text.toString()
)
clearMessageComposition()
alertDialog.dismiss()
}
cancelButton.setOnClickListener { alertDialog.dismiss() }
alertDialog.show()
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui package chat.rocket.android.chatroom.ui
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
...@@ -14,6 +15,11 @@ import android.view.Menu ...@@ -14,6 +15,11 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.FrameLayout
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.text.bold import androidx.core.text.bold
import androidx.core.view.isVisible import androidx.core.view.isVisible
...@@ -119,13 +125,12 @@ private const val MENU_ACTION_FAVORITE_MESSAGES = 5 ...@@ -119,13 +125,12 @@ private const val MENU_ACTION_FAVORITE_MESSAGES = 5
private const val MENU_ACTION_FILES = 6 private const val MENU_ACTION_FILES = 6
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener { class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
@Inject @Inject
lateinit var presenter: ChatRoomPresenter lateinit var presenter: ChatRoomPresenter
@Inject @Inject
lateinit var parser: MessageParser lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter private lateinit var adapter: ChatRoomAdapter
private lateinit var chatRoomId: String internal lateinit var chatRoomId: String
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
private var chatRoomMessage: String? = null private var chatRoomMessage: String? = null
...@@ -137,7 +142,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -137,7 +142,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var chatRoomLastSeen: Long = -1 private var chatRoomLastSeen: Long = -1
private lateinit var actionSnackbar: ActionSnackbar private lateinit var actionSnackbar: ActionSnackbar
private var citation: String? = null internal var citation: String? = null
private var editingMessageId: String? = null private var editingMessageId: String? = null
private val compositeDisposable = CompositeDisposable() private val compositeDisposable = CompositeDisposable()
...@@ -161,6 +166,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -161,6 +166,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private val handler = Handler() private val handler = Handler()
private var verticalScrollOffset = AtomicInteger(0) private var verticalScrollOffset = AtomicInteger(0)
private val dialogView by lazy { View.inflate(context, R.layout.file_attachments_dialog, null) }
internal val alertDialog by lazy { AlertDialog.Builder(activity).setView(dialogView).create() }
internal val imagePreview by lazy { dialogView.findViewById<ImageView>(R.id.image_preview) }
internal val sendButton by lazy { dialogView.findViewById<Button>(R.id.button_send) }
internal val cancelButton by lazy { dialogView.findViewById<Button>(R.id.button_cancel) }
internal val description by lazy { dialogView.findViewById<EditText>(R.id.text_file_description) }
internal val audioVideoAttachment by lazy { dialogView.findViewById<FrameLayout>(R.id.audio_video_attachment) }
internal val textFile by lazy { dialogView.findViewById<TextView>(R.id.text_file_name) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
...@@ -231,16 +245,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -231,16 +245,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (resultData != null && resultCode == Activity.RESULT_OK) { if (resultData != null && resultCode == Activity.RESULT_OK) {
when (requestCode) { when (requestCode) {
REQUEST_CODE_FOR_PERFORM_SAF -> { REQUEST_CODE_FOR_PERFORM_SAF -> {
presenter.uploadFile(chatRoomId, resultData.data, "") showFileAttachmentDialog(resultData.data)
// 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.
} }
REQUEST_CODE_FOR_DRAW -> { REQUEST_CODE_FOR_DRAW -> {
presenter.uploadDrawingImage( showDrawAttachmentDialog(
chatRoomId, resultData.getByteArrayExtra(DRAWING_BYTE_ARRAY_EXTRA_DATA)
resultData.getByteArrayExtra(DRAWING_BYTE_ARRAY_EXTRA_DATA),
""
) )
// 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.
} }
} }
} }
......
package chat.rocket.android.chatrooms.adapter
class LoadingItemHolder : ItemHolder<Unit> {
override val data: Unit
get() = Unit
}
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter
import android.view.View
class LoadingViewHolder(itemView: View) : ViewHolder<ItemHolder<Unit>>(itemView) {
override fun bindViews(data: ItemHolder<Unit>) {
}
}
\ No newline at end of file
...@@ -18,8 +18,11 @@ import chat.rocket.android.util.extensions.avatarUrl ...@@ -18,8 +18,11 @@ import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.date import chat.rocket.android.util.extensions.date
import chat.rocket.android.util.extensions.localDateTime import chat.rocket.android.util.extensions.localDateTime
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.User
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.model.userStatusOf import chat.rocket.common.model.userStatusOf
import chat.rocket.core.model.Room
import chat.rocket.core.model.SpotlightResult
class RoomUiModelMapper( class RoomUiModelMapper(
private val context: Application, private val context: Application,
...@@ -34,7 +37,7 @@ class RoomUiModelMapper( ...@@ -34,7 +37,7 @@ class RoomUiModelMapper(
private val messageUnreadColor = ContextCompat.getColor(context, android.R.color.primary_text_light) private val messageUnreadColor = ContextCompat.getColor(context, android.R.color.primary_text_light)
private val messageColor = ContextCompat.getColor(context, R.color.colorSecondaryText) private val messageColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
fun map(rooms: List<ChatRoom>, grouped: Boolean): List<ItemHolder<*>> { fun map(rooms: List<ChatRoom>, grouped: Boolean = false): List<ItemHolder<*>> {
val list = ArrayList<ItemHolder<*>>(rooms.size + 4) val list = ArrayList<ItemHolder<*>>(rooms.size + 4)
var lastType: String? = null var lastType: String? = null
rooms.forEach { room -> rooms.forEach { room ->
...@@ -48,6 +51,47 @@ class RoomUiModelMapper( ...@@ -48,6 +51,47 @@ class RoomUiModelMapper(
return list return list
} }
fun map(spotlight: SpotlightResult): List<ItemHolder<*>> {
val list = ArrayList<ItemHolder<*>>(spotlight.users.size + spotlight.rooms.size)
spotlight.users.filterNot { it.username.isNullOrEmpty() }.forEach { user ->
list.add(RoomItemHolder(mapUser(user)))
}
spotlight.rooms.filterNot { it.name.isNullOrEmpty() }.forEach { room ->
list.add(RoomItemHolder(mapRoom(room)))
}
return list
}
private fun mapUser(user: User): RoomUiModel {
return with(user) {
val name = mapName(user.username!!, user.name, false)
val status = user.status
val avatar = serverUrl.avatarUrl(user.username!!)
RoomUiModel(
id = user.id,
name = name,
type = roomTypeOf(RoomType.DIRECT_MESSAGE),
avatar = avatar,
status = status
)
}
}
private fun mapRoom(room: Room): RoomUiModel {
return with(room) {
RoomUiModel(
id = id,
name = name!!,
type = type,
avatar = serverUrl.avatarUrl(name!!, isGroupOrChannel = true),
lastMessage = mapLastMessage(lastMessage?.sender?.username,
lastMessage?.sender?.name, lastMessage?.message)
)
}
}
fun map(chatRoom: ChatRoom): RoomUiModel { fun map(chatRoom: ChatRoom): RoomUiModel {
return with(chatRoom.chatRoom) { return with(chatRoom.chatRoom) {
val isUnread = alert || unread > 0 val isUnread = alert || unread > 0
...@@ -89,7 +133,7 @@ class RoomUiModelMapper( ...@@ -89,7 +133,7 @@ class RoomUiModelMapper(
} }
} }
private fun mapLastMessage(name: String?, fullName: String?, text: String?, unread: Boolean): CharSequence? { private fun mapLastMessage(name: String?, fullName: String?, text: String?, unread: Boolean = false): CharSequence? {
return if (!settings.showLastMessage()) { return if (!settings.showLastMessage()) {
null null
} else if (name != null && text != null) { } else if (name != null && text != null) {
......
...@@ -6,12 +6,13 @@ import android.view.View ...@@ -6,12 +6,13 @@ import android.view.View
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
import kotlinx.android.synthetic.main.item_chat.view.* import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.* import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class RoomViewHolder(itemView: View, private val listener: (String) -> Unit) : ViewHolder<RoomItemHolder>(itemView) { class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit) : ViewHolder<RoomItemHolder>(itemView) {
private val resources: Resources = itemView.resources private val resources: Resources = itemView.resources
private val channelUnread: Drawable = resources.getDrawable(R.drawable.ic_hashtag_black_12dp) private val channelUnread: Drawable = resources.getDrawable(R.drawable.ic_hashtag_black_12dp)
...@@ -57,7 +58,7 @@ class RoomViewHolder(itemView: View, private val listener: (String) -> Unit) : V ...@@ -57,7 +58,7 @@ class RoomViewHolder(itemView: View, private val listener: (String) -> Unit) : V
} }
setOnClickListener { setOnClickListener {
listener(room.id) listener(room)
} }
} }
} }
......
...@@ -3,9 +3,10 @@ package chat.rocket.android.chatrooms.adapter ...@@ -3,9 +3,10 @@ package chat.rocket.android.chatrooms.adapter
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapter<ViewHolder<*>>() { class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) : RecyclerView.Adapter<ViewHolder<*>>() {
init { init {
setHasStableIds(true) setHasStableIds(true)
...@@ -18,14 +19,17 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte ...@@ -18,14 +19,17 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> {
if (viewType == 0) { if (viewType == VIEW_TYPE_ROOM) {
val view = parent.inflate(R.layout.item_chat) val view = parent.inflate(R.layout.item_chat)
return RoomViewHolder(view, listener) return RoomViewHolder(view, listener)
} else if (viewType == 1) { } else if (viewType == VIEW_TYPE_HEADER) {
val view = parent.inflate(R.layout.item_chatroom_header) val view = parent.inflate(R.layout.item_chatroom_header)
return HeaderViewHolder(view) return HeaderViewHolder(view)
} else if (viewType == VIEW_TYPE_LOADING) {
val view = parent.inflate(R.layout.item_loading)
return LoadingViewHolder(view)
} }
throw IllegalStateException("View type must be either Room or Header") throw IllegalStateException("View type must be either Room, Header or Loading")
} }
override fun getItemCount() = values.size override fun getItemCount() = values.size
...@@ -35,15 +39,17 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte ...@@ -35,15 +39,17 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte
return when(item) { return when(item) {
is HeaderItemHolder -> item.data.hashCode().toLong() is HeaderItemHolder -> item.data.hashCode().toLong()
is RoomItemHolder -> item.data.id.hashCode().toLong() is RoomItemHolder -> item.data.id.hashCode().toLong()
else -> throw IllegalStateException("View type must be either Room or Header") is LoadingItemHolder -> "loading".hashCode().toLong()
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
} }
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return when(values[position]) { return when(values[position]) {
is RoomItemHolder -> 0 is RoomItemHolder -> VIEW_TYPE_ROOM
is HeaderItemHolder -> 1 is HeaderItemHolder -> VIEW_TYPE_HEADER
else -> throw IllegalStateException("View type must be either Room or Header") is LoadingItemHolder -> VIEW_TYPE_LOADING
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
} }
} }
...@@ -55,4 +61,9 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte ...@@ -55,4 +61,9 @@ class RoomsAdapter(private val listener: (String) -> Unit) : RecyclerView.Adapte
} }
} }
companion object {
const val VIEW_TYPE_ROOM = 1
const val VIEW_TYPE_HEADER = 2
const val VIEW_TYPE_LOADING = 3
}
} }
\ No newline at end of file
...@@ -8,9 +8,9 @@ data class RoomUiModel( ...@@ -8,9 +8,9 @@ data class RoomUiModel(
val type: RoomType, val type: RoomType,
val name: CharSequence, val name: CharSequence,
val avatar: String, val avatar: String,
val date: CharSequence?, val date: CharSequence? = null,
val unread: String?, val unread: String? = null,
val alert: Boolean, val alert: Boolean = false,
val lastMessage: CharSequence?, val lastMessage: CharSequence? = null,
val status: UserStatus? val status: UserStatus? = null
) )
\ No newline at end of file
...@@ -71,7 +71,8 @@ class ChatRoomsFragmentModule { ...@@ -71,7 +71,8 @@ class ChatRoomsFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideFetchChatRoomsInteractor(client: RocketChatClient, dbManager: DatabaseManager): FetchChatRoomsInteractor { fun provideFetchChatRoomsInteractor(client: RocketChatClient,
dbManager: DatabaseManager): FetchChatRoomsInteractor {
return FetchChatRoomsInteractor(client, dbManager) return FetchChatRoomsInteractor(client, dbManager)
} }
......
...@@ -3,13 +3,12 @@ package chat.rocket.android.chatrooms.domain ...@@ -3,13 +3,12 @@ package chat.rocket.android.chatrooms.domain
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.model.ChatRoomEntity import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.android.db.model.UserEntity import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.chatRooms import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.userId import chat.rocket.core.model.userId
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
class FetchChatRoomsInteractor( class FetchChatRoomsInteractor(
...@@ -18,8 +17,6 @@ class FetchChatRoomsInteractor( ...@@ -18,8 +17,6 @@ class FetchChatRoomsInteractor(
) { ) {
suspend fun refreshChatRooms() { suspend fun refreshChatRooms() {
launch(CommonPool) {
try {
val rooms = retryIO("fetch chatRooms", times = 10, val rooms = retryIO("fetch chatRooms", times = 10,
initialDelay = 200, maxDelay = 2000) { initialDelay = 200, maxDelay = 2000) {
client.chatRooms().update.map { room -> client.chatRooms().update.map { room ->
...@@ -29,10 +26,6 @@ class FetchChatRoomsInteractor( ...@@ -29,10 +26,6 @@ class FetchChatRoomsInteractor(
Timber.d("Refreshing rooms: $rooms") Timber.d("Refreshing rooms: $rooms")
dbManager.insert(rooms) dbManager.insert(rooms)
} catch (ex: Exception) {
Timber.d(ex, "Error getting chatrooms")
}
}
} }
private suspend fun mapChatRoom(room: ChatRoom): ChatRoomEntity { private suspend fun mapChatRoom(room: ChatRoom): ChatRoomEntity {
......
...@@ -5,7 +5,7 @@ import chat.rocket.android.db.ChatRoomDao ...@@ -5,7 +5,7 @@ import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.model.ChatRoom import chat.rocket.android.db.model.ChatRoom
import javax.inject.Inject import javax.inject.Inject
class ChatRoomsRepository @Inject constructor(val dao: ChatRoomDao){ class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao){
fun getChatRooms(order: Order): LiveData<List<ChatRoom>> { fun getChatRooms(order: Order): LiveData<List<ChatRoom>> {
return when(order) { return when(order) {
Order.ACTIVITY -> dao.getAll() Order.ACTIVITY -> dao.getAll()
...@@ -15,6 +15,10 @@ class ChatRoomsRepository @Inject constructor(val dao: ChatRoomDao){ ...@@ -15,6 +15,10 @@ class ChatRoomsRepository @Inject constructor(val dao: ChatRoomDao){
} }
} }
fun search(query: String) = dao.searchSync(query)
fun count() = dao.count()
enum class Order { enum class Order {
ACTIVITY, ACTIVITY,
GROUPED_ACTIVITY, GROUPED_ACTIVITY,
......
package chat.rocket.android.chatrooms.presentation package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.android.helper.UserHelper import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.presentation.MainNavigator import chat.rocket.android.main.presentation.MainNavigator
...@@ -26,6 +29,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -26,6 +29,7 @@ class ChatRoomsPresenter @Inject constructor(
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: MainNavigator, private val navigator: MainNavigator,
@Named("currentServer") private val currentServer: String, @Named("currentServer") private val currentServer: String,
private val dbManager: DatabaseManager,
manager: ConnectionManager, manager: ConnectionManager,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val userHelper: UserHelper, private val userHelper: UserHelper,
...@@ -34,8 +38,33 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -34,8 +38,33 @@ class ChatRoomsPresenter @Inject constructor(
private val client = manager.client private val client = manager.client
private val settings = settingsRepository.get(currentServer) private val settings = settingsRepository.get(currentServer)
fun loadChatRoom(chatRoom: chat.rocket.android.db.model.ChatRoom) { fun loadChatRoom(chatRoom: RoomUiModel) {
with(chatRoom.chatRoom) { launchUI(strategy) {
view.showLoadingRoom(chatRoom.name)
try {
val room = dbManager.getRoom(chatRoom.id)
if (room != null) {
loadChatRoom(room.chatRoom)
} else {
with(chatRoom) {
val entity = ChatRoomEntity(
id = id,
subscriptionId = "",
type = type.toString(),
name = name.toString(),
open = false
)
loadChatRoom(entity)
}
}
} finally {
view.hideLoadingRoom()
}
}
}
fun loadChatRoom(chatRoom: ChatRoomEntity) {
with(chatRoom) {
val isDirectMessage = roomTypeOf(type) is RoomType.DirectMessage val isDirectMessage = roomTypeOf(type) is RoomType.DirectMessage
val roomName = if (settings.useSpecialCharsOnRoom() || (isDirectMessage && settings.useRealName())) { val roomName = if (settings.useSpecialCharsOnRoom() || (isDirectMessage && settings.useRealName())) {
fullname ?: name fullname ?: name
......
...@@ -2,22 +2,9 @@ package chat.rocket.android.chatrooms.presentation ...@@ -2,22 +2,9 @@ package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
interface ChatRoomsView : LoadingView, MessageView { interface ChatRoomsView : LoadingView, MessageView {
fun showLoadingRoom(name: CharSequence)
/** fun hideLoadingRoom()
* Shows the chat rooms.
*
* @param newDataSet The new data set to show.
*/
suspend fun updateChatRooms(newDataSet: List<ChatRoom>)
/**
* Shows no chat rooms to display.
*/
fun showNoChatRoomsToDisplay()
fun showConnectionState(state: State)
} }
\ No newline at end of file
package chat.rocket.android.chatrooms.ui package chat.rocket.android.chatrooms.ui
import android.app.AlertDialog import android.app.AlertDialog
import android.app.ProgressDialog
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.view.LayoutInflater import android.view.LayoutInflater
...@@ -21,27 +22,25 @@ import androidx.recyclerview.widget.DefaultItemAnimator ...@@ -21,27 +22,25 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.RoomsAdapter import chat.rocket.android.chatrooms.adapter.RoomsAdapter
import chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository
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
import chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModel import chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModel
import chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModelFactory import chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModelFactory
import chat.rocket.android.chatrooms.viewmodel.LoadingState
import chat.rocket.android.chatrooms.viewmodel.Query
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.helper.ChatRoomsSortOrder import chat.rocket.android.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants import chat.rocket.android.helper.Constants
import chat.rocket.android.helper.SharedPreferenceHelper import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.widget.DividerItemDecoration import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_rooms.* import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -58,9 +57,12 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -58,9 +57,12 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
lateinit var viewModel: ChatRoomsViewModel lateinit var viewModel: ChatRoomsViewModel
private var searchView: SearchView? = null private var searchView: SearchView? = null
private var sortView: MenuItem? = null
private val handler = Handler() private val handler = Handler()
private var chatRoomId: String? = null private var chatRoomId: String? = null
private var progressDialog: ProgressDialog? = null
companion object { companion object {
fun newInstance(chatRoomId: String? = null): ChatRoomsFragment { fun newInstance(chatRoomId: String? = null): ChatRoomsFragment {
...@@ -109,17 +111,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -109,17 +111,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
private fun subscribeUi() { private fun subscribeUi() {
ui { ui {
val adapter = RoomsAdapter { room ->
val adapter = RoomsAdapter { roomId ->
launch(UI) {
dbManager.getRoom(roomId)?.let { room ->
ui {
presenter.loadChatRoom(room) presenter.loadChatRoom(room)
} }
}
}
}
recycler_view.layoutManager = LinearLayoutManager(it) recycler_view.layoutManager = LinearLayoutManager(it)
recycler_view.addItemDecoration(DividerItemDecoration(it, recycler_view.addItemDecoration(DividerItemDecoration(it,
...@@ -132,6 +126,23 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -132,6 +126,23 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
rooms?.let { rooms?.let {
Timber.d("Got items: $it") Timber.d("Got items: $it")
adapter.values = it adapter.values = it
if (rooms.isNotEmpty()) {
text_no_data_to_display.isVisible = false
}
}
})
viewModel.loadingState.observe(viewLifecycleOwner, Observer { state ->
when(state) {
is LoadingState.Loading -> if (state.count == 0L) showLoading()
is LoadingState.Loaded -> {
hideLoading()
if (state.count == 0L) showNoChatRoomsToDisplay()
}
is LoadingState.Error -> {
hideLoading()
showGenericErrorMessage()
}
} }
}) })
...@@ -147,6 +158,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -147,6 +158,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.chatrooms, menu) inflater.inflate(R.menu.chatrooms, menu)
sortView = menu.findItem(R.id.action_sort)
val searchItem = menu.findItem(R.id.action_search) val searchItem = menu.findItem(R.id.action_search)
searchView = searchItem?.actionView as? SearchView searchView = searchItem?.actionView as? SearchView
searchView?.setIconifiedByDefault(false) searchView?.setIconifiedByDefault(false)
...@@ -160,6 +173,21 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -160,6 +173,21 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
return queryChatRoomsByName(newText) return queryChatRoomsByName(newText)
} }
}) })
val expandListener = object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
// Simply setting sortView to visible won't work, so we invalidate the options
// to recreate the entire menu...
activity?.invalidateOptionsMenu()
return true
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
sortView?.isVisible = false
return true
}
}
searchItem?.setOnActionExpandListener(expandListener)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
...@@ -209,25 +237,17 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -209,25 +237,17 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY) val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY)
val grouped = SharedPreferenceHelper.getBoolean(Constants.CHATROOM_GROUP_BY_TYPE_KEY, false) val grouped = SharedPreferenceHelper.getBoolean(Constants.CHATROOM_GROUP_BY_TYPE_KEY, false)
val order = when(sortType) { val query = when(sortType) {
ChatRoomsSortOrder.ALPHABETICAL -> { ChatRoomsSortOrder.ALPHABETICAL -> {
if (grouped) { Query.ByName(grouped)
ChatRoomsRepository.Order.GROUPED_NAME
} else {
ChatRoomsRepository.Order.NAME
}
} }
ChatRoomsSortOrder.ACTIVITY -> { ChatRoomsSortOrder.ACTIVITY -> {
if (grouped) { Query.ByActivity(grouped)
ChatRoomsRepository.Order.GROUPED_ACTIVITY
} else {
ChatRoomsRepository.Order.ACTIVITY
}
} }
else -> ChatRoomsRepository.Order.ACTIVITY else -> Query.ByActivity()
} }
viewModel.setOrdering(order) viewModel.setQuery(query)
} }
private fun invalidateQueryOnSearch() { private fun invalidateQueryOnSearch() {
...@@ -238,21 +258,17 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -238,21 +258,17 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
} }
override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) {} private fun showNoChatRoomsToDisplay() {
override fun showNoChatRoomsToDisplay() {
ui { text_no_data_to_display.isVisible = true } ui { text_no_data_to_display.isVisible = true }
} }
override fun showLoading() { override fun showLoading() {
ui { view_loading.isVisible = true } view_loading.isVisible = true
} }
override fun hideLoading() { override fun hideLoading() {
ui {
view_loading.isVisible = false view_loading.isVisible = false
} }
}
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
ui { ui {
...@@ -268,7 +284,17 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -268,7 +284,17 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showConnectionState(state: State) { override fun showLoadingRoom(name: CharSequence) {
ui {
progressDialog = ProgressDialog.show(activity, "Rocket.Chat", "Loading room $name")
}
}
override fun hideLoadingRoom() {
progressDialog?.dismiss()
}
private fun showConnectionState(state: State) {
Timber.d("Got new state: $state") Timber.d("Got new state: $state")
ui { ui {
connection_status_text.fadeIn() connection_status_text.fadeIn()
...@@ -298,7 +324,11 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -298,7 +324,11 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
private fun queryChatRoomsByName(name: String?): Boolean { private fun queryChatRoomsByName(name: String?): Boolean {
//presenter.chatRoomsByName(name ?: "") if (name.isNullOrEmpty()) {
updateSort()
} else {
viewModel.setQuery(Query.Search(name!!))
}
return true return true
} }
} }
\ No newline at end of file
...@@ -5,19 +5,30 @@ import androidx.lifecycle.MutableLiveData ...@@ -5,19 +5,30 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import chat.rocket.android.chatrooms.adapter.ItemHolder import chat.rocket.android.chatrooms.adapter.ItemHolder
import chat.rocket.android.chatrooms.adapter.LoadingItemHolder
import chat.rocket.android.chatrooms.adapter.RoomUiModelMapper import chat.rocket.android.chatrooms.adapter.RoomUiModelMapper
import chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor import chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository import chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository
import chat.rocket.android.chatrooms.infrastructure.isGrouped
import chat.rocket.android.server.infraestructure.ConnectionManager import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.util.livedata.transform import chat.rocket.android.util.livedata.transform
import chat.rocket.android.util.livedata.wrap
import chat.rocket.android.util.retryIO
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.model.SpotlightResult
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.isActive
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.withContext
import me.henrytao.livedataktx.distinct import me.henrytao.livedataktx.distinct
import me.henrytao.livedataktx.map import me.henrytao.livedataktx.map
import me.henrytao.livedataktx.nonNull import me.henrytao.livedataktx.nonNull
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException
import kotlin.coroutines.experimental.coroutineContext
class ChatRoomsViewModel( class ChatRoomsViewModel(
private val connectionManager: ConnectionManager, private val connectionManager: ConnectionManager,
...@@ -25,31 +36,65 @@ class ChatRoomsViewModel( ...@@ -25,31 +36,65 @@ class ChatRoomsViewModel(
private val repository: ChatRoomsRepository, private val repository: ChatRoomsRepository,
private val mapper: RoomUiModelMapper private val mapper: RoomUiModelMapper
) : ViewModel() { ) : ViewModel() {
private val ordering: MutableLiveData<ChatRoomsRepository.Order> = MutableLiveData() private val query = MutableLiveData<Query>()
val loadingState = MutableLiveData<LoadingState>()
private val runContext = newSingleThreadContext("chat-rooms-view-model") private val runContext = newSingleThreadContext("chat-rooms-view-model")
private val client = connectionManager.client
private var loaded = false
init { fun getChatRooms(): LiveData<RoomsModel> {
ordering.value = ChatRoomsRepository.Order.ACTIVITY return Transformations.switchMap(query) { query ->
} return@switchMap if (query.isSearch()) {
this@ChatRoomsViewModel.query.wrap(runContext) { _, data: MutableLiveData<RoomsModel> ->
val string = (query as Query.Search).query
// debounce, to not query while the user is writing
delay(200)
// TODO - find a better way for cancellation checking
if (!coroutineContext.isActive) return@wrap
val rooms = repository.search(string).let { mapper.map(it) }
data.postValue(rooms.toMutableList() + LoadingItemHolder())
if (!coroutineContext.isActive) return@wrap
val spotlight = spotlight(query.query)?.let { mapper.map(it) }
if (!coroutineContext.isActive) return@wrap
fun getChatRooms(): LiveData<List<ItemHolder<*>>> { spotlight?.let {
return Transformations.switchMap(ordering) { order -> data.postValue(rooms.toMutableList() + spotlight)
Timber.d("Querying rooms for order: $order") }.ifNull {
repository.getChatRooms(order) data.postValue(rooms)
}
}
} else {
repository.getChatRooms(query.asSortingOrder())
.nonNull() .nonNull()
.distinct() .distinct()
.transform(runContext) { rooms -> .transform(runContext) { rooms ->
rooms?.let { val mappedRooms = rooms?.let {
mapper.map(rooms, order.isGrouped()) mapper.map(rooms, query.isGrouped())
}
if (loaded && mappedRooms?.isEmpty() == true) {
loadingState.postValue(LoadingState.Loaded(0))
}
mappedRooms
} }
} }
} }
} }
private suspend fun spotlight(query: String): SpotlightResult? {
return try {
retryIO { client.spotlight(query) }
} catch (ex: Exception) {
ex.printStackTrace()
null
}
}
fun getStatus(): MutableLiveData<State> { fun getStatus(): MutableLiveData<State> {
return connectionManager.statusLiveData.nonNull().distinct().map { state -> return connectionManager.statusLiveData.nonNull().distinct().map { state ->
if (state is State.Connected) { if (state is State.Connected) {
// TODO - add a loading status...
fetchRooms() fetchRooms()
} }
state state
...@@ -58,11 +103,69 @@ class ChatRoomsViewModel( ...@@ -58,11 +103,69 @@ class ChatRoomsViewModel(
private fun fetchRooms() { private fun fetchRooms() {
launch { launch {
setLoadingState(LoadingState.Loading(repository.count()))
try {
interactor.refreshChatRooms() interactor.refreshChatRooms()
setLoadingState(LoadingState.Loaded(repository.count()))
loaded = true
} catch (ex: Exception) {
Timber.d(ex, "Error refreshing chatrooms")
setLoadingState(LoadingState.Error(repository.count()))
}
} }
} }
fun setOrdering(order: ChatRoomsRepository.Order) { fun setQuery(query: Query) {
ordering.value = order this.query.value = query
}
private suspend fun setLoadingState(state: LoadingState) {
withContext(UI) {
loadingState.value = state
}
}
}
typealias RoomsModel = List<ItemHolder<*>>
sealed class LoadingState {
data class Loading(val count: Long) : LoadingState()
data class Loaded(val count: Long) : LoadingState()
data class Error(val count: Long) : LoadingState()
}
sealed class Query {
data class ByActivity(val grouped: Boolean = false) : Query()
data class ByName(val grouped: Boolean = false) : Query()
data class Search(val query: String) : Query()
}
fun Query.isSearch(): Boolean = this is Query.Search
fun Query.isGrouped(): Boolean {
return when(this) {
is Query.Search -> false
is Query.ByName -> grouped
is Query.ByActivity -> grouped
}
}
fun Query.asSortingOrder(): ChatRoomsRepository.Order {
return when(this) {
is Query.ByName -> {
if (grouped) {
ChatRoomsRepository.Order.GROUPED_NAME
} else {
ChatRoomsRepository.Order.NAME
}
}
is Query.ByActivity -> {
if (grouped) {
ChatRoomsRepository.Order.GROUPED_ACTIVITY
} else {
ChatRoomsRepository.Order.ACTIVITY
}
}
else -> throw InvalidParameterException("Should be ByName or ByActivity")
} }
} }
...@@ -20,6 +20,17 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> { ...@@ -20,6 +20,17 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
""") """)
abstract fun get(id: String): ChatRoom? abstract fun get(id: String): ChatRoom?
@Transaction
@Query("$BASE_QUERY")
abstract fun getAllSync(): List<ChatRoom>
@Transaction
@Query("""$BASE_QUERY WHERE chatrooms.name LIKE '%' || :query || '%' OR users.name LIKE '%' || :query || '%'""")
abstract fun searchSync(query: String): List<ChatRoom>
@Query("SELECT COUNT(id) FROM chatrooms")
abstract fun count(): Long
@Transaction @Transaction
@Query(""" @Query("""
$BASE_QUERY $BASE_QUERY
...@@ -62,6 +73,15 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> { ...@@ -62,6 +73,15 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
@Query("DELETE FROM chatrooms WHERE ID = :id") @Query("DELETE FROM chatrooms WHERE ID = :id")
abstract fun delete(id: String) abstract fun delete(id: String)
@Query("DELETE FROM chatrooms")
abstract fun delete()
@Transaction
open fun cleanInsert(chatRooms: List<ChatRoomEntity>) {
delete()
insert(chatRooms)
}
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insertOrReplace(chatRooms: List<ChatRoomEntity>) abstract fun insertOrReplace(chatRooms: List<ChatRoomEntity>)
......
...@@ -35,6 +35,10 @@ class DatabaseManager(val context: Application, ...@@ -35,6 +35,10 @@ class DatabaseManager(val context: Application,
fun chatRoomDao(): ChatRoomDao = database.chatRoomDao() fun chatRoomDao(): ChatRoomDao = database.chatRoomDao()
fun userDao(): UserDao = database.userDao() fun userDao(): UserDao = database.userDao()
fun logout() {
database.clearAllTables()
}
suspend fun getRoom(id: String) = withContext(dbContext) { suspend fun getRoom(id: String) = withContext(dbContext) {
chatRoomDao().get(id) chatRoomDao().get(id)
} }
...@@ -297,7 +301,7 @@ class DatabaseManager(val context: Application, ...@@ -297,7 +301,7 @@ class DatabaseManager(val context: Application,
suspend fun insert(rooms: List<ChatRoomEntity>) { suspend fun insert(rooms: List<ChatRoomEntity>) {
withContext(dbContext) { withContext(dbContext) {
chatRoomDao().insert(rooms) chatRoomDao().cleanInsert(rooms)
} }
} }
......
...@@ -24,9 +24,9 @@ data class ChatRoomEntity( ...@@ -24,9 +24,9 @@ data class ChatRoomEntity(
var subscriptionId: String, var subscriptionId: String,
var type: String, var type: String,
var name: String, var name: String,
var fullname: String?, var fullname: String? = null,
var userId: String?, var userId: String? = null,
var ownerId: String?, var ownerId: String? = null,
var readonly: Boolean? = false, var readonly: Boolean? = false,
var isDefault: Boolean? = false, var isDefault: Boolean? = false,
var favorite: Boolean? = false, var favorite: Boolean? = false,
...@@ -38,9 +38,9 @@ data class ChatRoomEntity( ...@@ -38,9 +38,9 @@ data class ChatRoomEntity(
var updatedAt: Long? = -1, var updatedAt: Long? = -1,
var timestamp: Long? = -1, var timestamp: Long? = -1,
var lastSeen: Long? = -1, var lastSeen: Long? = -1,
var lastMessageText: String?, var lastMessageText: String? = null,
var lastMessageUserId: String?, var lastMessageUserId: String? = null,
var lastMessageTimestamp: Long? var lastMessageTimestamp: Long? = null
) )
data class ChatRoom( data class ChatRoom(
......
...@@ -18,6 +18,8 @@ interface LocalRepository { ...@@ -18,6 +18,8 @@ interface LocalRepository {
fun clearAllFromServer(server: String) fun clearAllFromServer(server: String)
fun getCurrentUser(url: String): User? fun getCurrentUser(url: String): User?
fun saveCurrentUser(url: String, user: User) fun saveCurrentUser(url: String, user: User)
fun saveLastChatroomsRefresh(url: String, timestamp: Long)
fun getLastChatroomsRefresh(url: String): Long
companion object { companion object {
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN" const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
...@@ -26,6 +28,7 @@ interface LocalRepository { ...@@ -26,6 +28,7 @@ interface LocalRepository {
const val PERMISSIONS_KEY = "permissions_" const val PERMISSIONS_KEY = "permissions_"
const val USER_KEY = "user_" const val USER_KEY = "user_"
const val CURRENT_USERNAME_KEY = "username_" const val CURRENT_USERNAME_KEY = "username_"
const val LAST_CHATROOMS_REFRESH = "_chatrooms_refresh"
} }
} }
......
...@@ -22,6 +22,13 @@ class SharedPreferencesLocalRepository( ...@@ -22,6 +22,13 @@ class SharedPreferencesLocalRepository(
save("${url}_${LocalRepository.USER_KEY}", userAdapter.toJson(user)) save("${url}_${LocalRepository.USER_KEY}", userAdapter.toJson(user))
} }
override fun saveLastChatroomsRefresh(url: String, timestamp: Long) {
save("$url${LocalRepository.LAST_CHATROOMS_REFRESH}", timestamp)
}
override fun getLastChatroomsRefresh(url: String) =
getLong("$url${LocalRepository.LAST_CHATROOMS_REFRESH}", 0L)
override fun getBoolean(key: String, defValue: Boolean) = preferences.getBoolean(key, defValue) override fun getBoolean(key: String, defValue: Boolean) = preferences.getBoolean(key, defValue)
override fun getFloat(key: String, defValue: Float) = preferences.getFloat(key, defValue) override fun getFloat(key: String, defValue: Float) = preferences.getFloat(key, defValue)
......
package chat.rocket.android.main.presentation package chat.rocket.android.main.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.uimodel.NavHeaderUiModel import chat.rocket.android.main.uimodel.NavHeaderUiModel
import chat.rocket.android.main.uimodel.NavHeaderUiModelMapper import chat.rocket.android.main.uimodel.NavHeaderUiModelMapper
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.RemoveAccountInteractor
import chat.rocket.android.server.domain.SaveAccountInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.favicon
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
...@@ -23,7 +31,9 @@ import chat.rocket.core.internal.rest.logout ...@@ -23,7 +31,9 @@ import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -39,11 +49,13 @@ class MainPresenter @Inject constructor( ...@@ -39,11 +49,13 @@ class MainPresenter @Inject constructor(
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInteractor: RemoveAccountInteractor, private val removeAccountInteractor: RemoveAccountInteractor,
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
dbManagerFactory: DatabaseManagerFactory,
getSettingsInteractor: GetSettingsInteractor, getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory managerFactory: ConnectionManagerFactory
) : CheckServerPresenter(strategy, factory, view = view) { ) : CheckServerPresenter(strategy, factory, view = view) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer) private val manager = managerFactory.create(currentServer)
private val dbManager = dbManagerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!) private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
...@@ -91,6 +103,7 @@ class MainPresenter @Inject constructor( ...@@ -91,6 +103,7 @@ class MainPresenter @Inject constructor(
*/ */
fun logout() { fun logout() {
launchUI(strategy) { launchUI(strategy) {
view.showProgress()
try { try {
clearTokens() clearTokens()
retryIO("logout") { client.logout() } retryIO("logout") { client.logout() }
...@@ -107,10 +120,13 @@ class MainPresenter @Inject constructor( ...@@ -107,10 +120,13 @@ class MainPresenter @Inject constructor(
disconnect() disconnect()
removeAccountInteractor.remove(currentServer) removeAccountInteractor.remove(currentServer)
tokenRepository.remove(currentServer) tokenRepository.remove(currentServer)
withContext(CommonPool) { dbManager.logout() }
navigator.toNewServer() navigator.toNewServer()
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error cleaning up the session...") Timber.d(ex, "Error cleaning up the session...")
} }
view.hideProgress()
} }
} }
......
...@@ -26,4 +26,7 @@ interface MainView : MessageView, VersionCheckView { ...@@ -26,4 +26,7 @@ interface MainView : MessageView, VersionCheckView {
fun closeServerSelection() fun closeServerSelection()
fun invalidateToken(token: String) fun invalidateToken(token: String)
fun showProgress()
fun hideProgress()
} }
\ No newline at end of file
...@@ -3,6 +3,7 @@ package chat.rocket.android.main.ui ...@@ -3,6 +3,7 @@ package chat.rocket.android.main.ui
import DrawableHelper import DrawableHelper
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.app.ProgressDialog
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
...@@ -269,4 +270,14 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -269,4 +270,14 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
fun setCheckedNavDrawerItem(@IdRes item: Int) { fun setCheckedNavDrawerItem(@IdRes item: Int) {
view_navigation.setCheckedItem(item) view_navigation.setCheckedItem(item)
} }
private var progressDialog : ProgressDialog? = null
override fun showProgress() {
progressDialog = ProgressDialog.show(this, getString(R.string.app_name), getString(R.string.msg_log_out), true, false)
}
override fun hideProgress() {
progressDialog?.dismiss()
progressDialog = null
}
} }
\ No newline at end of file
...@@ -2,6 +2,7 @@ package chat.rocket.android.server.infraestructure ...@@ -2,6 +2,7 @@ package chat.rocket.android.server.infraestructure
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.common.model.BaseRoom import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.User import chat.rocket.common.model.User
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
...@@ -40,10 +41,8 @@ class ConnectionManager( ...@@ -40,10 +41,8 @@ class ConnectionManager(
private val statusChannel = Channel<State>(Channel.CONFLATED) private val statusChannel = Channel<State>(Channel.CONFLATED)
private var connectJob: Job? = null private var connectJob: Job? = null
private val roomAndSubscriptionChannels = ArrayList<Channel<StreamMessage<BaseRoom>>>()
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>() private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val userDataChannels = ArrayList<Channel<Myself>>() private val userDataChannels = ArrayList<Channel<Myself>>()
private val activeUsersChannels = ArrayList<Channel<User>>()
private val subscriptionIdMap = HashMap<String, String>() private val subscriptionIdMap = HashMap<String, String>()
private var subscriptionId: String? = null private var subscriptionId: String? = null
...@@ -126,9 +125,6 @@ class ConnectionManager( ...@@ -126,9 +125,6 @@ class ConnectionManager(
for (room in client.roomsChannel) { for (room in client.roomsChannel) {
Timber.d("GOT Room streamed") Timber.d("GOT Room streamed")
roomsActor.send(room) roomsActor.send(room)
for (channel in roomAndSubscriptionChannels) {
channel.send(room)
}
} }
} }
...@@ -137,9 +133,6 @@ class ConnectionManager( ...@@ -137,9 +133,6 @@ class ConnectionManager(
for (subscription in client.subscriptionsChannel) { for (subscription in client.subscriptionsChannel) {
Timber.d("GOT Subscription streamed") Timber.d("GOT Subscription streamed")
roomsActor.send(subscription) roomsActor.send(subscription)
for (channel in roomAndSubscriptionChannels) {
channel.send(subscription)
}
} }
} }
...@@ -170,9 +163,6 @@ class ConnectionManager( ...@@ -170,9 +163,6 @@ class ConnectionManager(
totalUsers++ totalUsers++
//Timber.d("Got activeUsers: $totalUsers") //Timber.d("Got activeUsers: $totalUsers")
userActor.send(user) userActor.send(user)
for (channel in activeUsersChannels) {
channel.send(user)
}
} }
} }
...@@ -205,20 +195,10 @@ class ConnectionManager( ...@@ -205,20 +195,10 @@ class ConnectionManager(
fun removeStatusChannel(channel: Channel<State>) = statusChannelList.remove(channel) fun removeStatusChannel(channel: Channel<State>) = statusChannelList.remove(channel)
fun addRoomsAndSubscriptionsChannel(channel: Channel<StreamMessage<BaseRoom>>) =
roomAndSubscriptionChannels.add(channel)
fun removeRoomsAndSubscriptionsChannel(channel: Channel<StreamMessage<BaseRoom>>) =
roomAndSubscriptionChannels.remove(channel)
fun addUserDataChannel(channel: Channel<Myself>) = userDataChannels.add(channel) fun addUserDataChannel(channel: Channel<Myself>) = userDataChannels.add(channel)
fun removeUserDataChannel(channel: Channel<Myself>) = userDataChannels.remove(channel) fun removeUserDataChannel(channel: Channel<Myself>) = userDataChannels.remove(channel)
fun addActiveUserChannel(channel: Channel<User>) = activeUsersChannels.add(channel)
fun removeActiveUserChannel(channel: Channel<User>) = activeUsersChannels.remove(channel)
fun subscribeRoomMessages(roomId: String, channel: Channel<Message>) { fun subscribeRoomMessages(roomId: String, channel: Channel<Message>) {
val oldSub = roomMessagesChannels.put(roomId, channel) val oldSub = roomMessagesChannels.put(roomId, channel)
if (oldSub != null) { if (oldSub != null) {
......
package chat.rocket.android.server.infraestructure package chat.rocket.android.server.infraestructure
import chat.rocket.android.db.DatabaseManagerFactory import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.infrastructure.LocalRepository
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
......
package chat.rocket.android.util package chat.rocket.android.util
import chat.rocket.android.BuildConfig
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType import okhttp3.MediaType
...@@ -28,6 +29,8 @@ class HttpLoggingInterceptor constructor(private val logger: Logger) : Intercept ...@@ -28,6 +29,8 @@ class HttpLoggingInterceptor constructor(private val logger: Logger) : Intercept
@Volatile @Volatile
internal var level = Level.NONE internal var level = Level.NONE
private val isDebug = BuildConfig.DEBUG
enum class Level { enum class Level {
/** No logs. */ /** No logs. */
NONE, NONE,
...@@ -140,7 +143,7 @@ class HttpLoggingInterceptor constructor(private val logger: Logger) : Intercept ...@@ -140,7 +143,7 @@ class HttpLoggingInterceptor constructor(private val logger: Logger) : Intercept
val name = headers.name(i) val name = headers.name(i)
// Skip headers from the request body as they are explicitly logged above. // Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equals(name, ignoreCase = true) && !"Content-Length".equals(name, ignoreCase = true)) { if (!"Content-Type".equals(name, ignoreCase = true) && !"Content-Length".equals(name, ignoreCase = true)) {
if ("X-Auth-Token".equals(name, ignoreCase = true)) { if (!isDebug && "X-Auth-Token".equals(name, ignoreCase = true)) {
logger.log("$name: ${skipAuthToken(headers.value(i).length)}") logger.log("$name: ${skipAuthToken(headers.value(i).length)}")
} else { } else {
logger.log("$name: ${headers.value(i)}") logger.log("$name: ${headers.value(i)}")
......
package chat.rocket.android.util package chat.rocket.android.util
import chat.rocket.common.RocketChatNetworkErrorException import chat.rocket.common.RocketChatNetworkErrorException
import kotlinx.coroutines.experimental.TimeoutCancellationException
import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.isActive
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.experimental.coroutineContext
const val DEFAULT_RETRY = 3 const val DEFAULT_RETRY = 3
...@@ -16,14 +19,19 @@ suspend fun <T> retryIO( ...@@ -16,14 +19,19 @@ suspend fun <T> retryIO(
{ {
var currentDelay = initialDelay var currentDelay = initialDelay
repeat(times - 1) { currentTry -> repeat(times - 1) { currentTry ->
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
try { try {
return block() return block()
} catch (e: RocketChatNetworkErrorException) { } catch (e: RocketChatNetworkErrorException) {
Timber.d(e, "failed call($currentTry): $description") Timber.d(e, "failed call($currentTry): $description")
e.printStackTrace() e.printStackTrace()
} }
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
delay(currentDelay) delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
} }
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
return block() // last attempt return block() // last attempt
} }
\ No newline at end of file
package chat.rocket.android.util.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.CoroutineScope
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import kotlin.coroutines.experimental.CoroutineContext
class WrappedLiveData<Source, Output>(
private val runContext: CoroutineContext = CommonPool,
private val source: LiveData<Source>,
private val transformation: suspend (Source?, MutableLiveData<Output>) -> Unit)
: MutableLiveData<Output>() {
private var job: Job? = null
private val observer = Observer<Source> { source ->
job?.cancel()
job = launch(runContext) {
transformation(source, this@WrappedLiveData)
}
}
override fun onActive() {
source.observeForever(observer)
}
override fun onInactive() {
job?.cancel()
source.removeObserver(observer)
}
}
fun <Source, Output> LiveData<Source>.wrap(
runContext: CoroutineContext = CommonPool,
transformation: suspend (Source?, MutableLiveData<Output>) -> Unit) =
WrappedLiveData(runContext, this, transformation)
\ No newline at end of file
...@@ -2,7 +2,9 @@ package chat.rocket.android.widget ...@@ -2,7 +2,9 @@ package chat.rocket.android.widget
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.View
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
...@@ -51,6 +53,9 @@ class DividerItemDecoration() : RecyclerView.ItemDecoration() { ...@@ -51,6 +53,9 @@ class DividerItemDecoration() : RecyclerView.ItemDecoration() {
for (i in 0 until childCount) { for (i in 0 until childCount) {
val child = parent.getChildAt(i) val child = parent.getChildAt(i)
if (isLastView(child, parent))
continue
val params = child.layoutParams as RecyclerView.LayoutParams val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin val top = child.bottom + params.bottomMargin
...@@ -60,4 +65,9 @@ class DividerItemDecoration() : RecyclerView.ItemDecoration() { ...@@ -60,4 +65,9 @@ class DividerItemDecoration() : RecyclerView.ItemDecoration() {
divider.draw(c) divider.draw(c)
} }
} }
private fun isLastView(view: View, parent: RecyclerView): Boolean {
val position = parent.getChildAdapterPosition(view)
return position == parent.adapter?.itemCount?.minus(1) ?: false
}
} }
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorAccent" />
<corners android:radius="4dp"/>
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp">
<TextView
android:id="@+id/text_dialog_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/msg_upload_file"
style="@style/Base.DialogWindowTitle.AppCompat"
android:layout_margin="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_file_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:drawableStart="@drawable/ic_files_24dp"
android:drawablePadding="6dp"
app:layout_constraintTop_toBottomOf="@id/text_dialog_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="16dp"
android:textDirection="locale"
android:visibility="gone"
tools:text="This is a very, very, very long filename, to test how the layout will work on very very very long filenames.pdf" />
<FrameLayout
android:id="@+id/audio_video_attachment"
android:layout_width="0dp"
android:layout_height="150dp"
android:background="@color/colorBlack"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_file_name"
android:layout_margin="16dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/exo_controls_play" />
</FrameLayout>
<ImageView
android:id="@+id/image_preview"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHeight_max="240dp"
app:layout_constrainedHeight="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/audio_video_attachment"
android:layout_margin="16dp"
android:adjustViewBounds="true"
android:visibility="gone"
tools:visibility="visible" />
<EditText
android:id="@+id/text_file_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/image_preview"
android:layout_margin="16dp"
android:hint="@string/msg_file_description" />
<Button
android:id="@+id/button_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_file_description"
android:layout_margin="16dp"
android:background="@drawable/rounded_color_accent"
android:text="@string/msg_send"
android:textColor="@color/colorWhite" />
<Button
android:id="@+id/button_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/text_file_description"
app:layout_constraintEnd_toStartOf="@id/button_send"
android:layout_margin="16dp"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:text="@string/msg_cancel" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</FrameLayout>
\ No newline at end of file
...@@ -133,6 +133,9 @@ ...@@ -133,6 +133,9 @@
// TODO: Add proper translation. // TODO: Add proper translation.
<string name="msg_search">Search</string> <string name="msg_search">Search</string>
<string name="msg_message_copied">Mensaje copiado</string> <string name="msg_message_copied">Mensaje copiado</string>
<string name="msg_upload_file">Subir archivo</string>
<string name="msg_file_description">Descripción del archivo</string>
<string name="msg_send">enviar</string>
// TODO: Add proper translation. // TODO: Add proper translation.
<string name="msg_delete_message">Delete Message</string> <string name="msg_delete_message">Delete Message</string>
<string name="msg_delete_description">Are you sure you want to delete this message</string> <string name="msg_delete_description">Are you sure you want to delete this message</string>
...@@ -150,7 +153,6 @@ ...@@ -150,7 +153,6 @@
<string name="msg_member_not_found">Member not found</string> <string name="msg_member_not_found">Member not found</string>
<string name="msg_channel_created_successfully">Channel created successfully</string> <string name="msg_channel_created_successfully">Channel created successfully</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Nombre de la sala cambiado para: %1$s por %2$s</string> <string name="message_room_name_changed">Nombre de la sala cambiado para: %1$s por %2$s</string>
<string name="message_user_added_by">Usuario %1$s añadido por %2$s</string> <string name="message_user_added_by">Usuario %1$s añadido por %2$s</string>
...@@ -282,4 +284,5 @@ ...@@ -282,4 +284,5 @@
<string name="notif_action_reply_hint">RESPUESTA</string> <string name="notif_action_reply_hint">RESPUESTA</string>
<string name="notif_error_sending">La respuesta ha fallado. Inténtalo de nuevo.</string> <string name="notif_error_sending">La respuesta ha fallado. Inténtalo de nuevo.</string>
<string name="notif_success_sending">Mensaje enviado a %1$s!</string> <string name="notif_success_sending">Mensaje enviado a %1$s!</string>
<string name="msg_log_out">Saliendo de tu cuenta...</string>
</resources> </resources>
...@@ -133,6 +133,9 @@ ...@@ -133,6 +133,9 @@
// TODO: Add proper translation. // TODO: Add proper translation.
<string name="msg_search">Search</string> <string name="msg_search">Search</string>
<string name="msg_message_copied">Message copié</string> <string name="msg_message_copied">Message copié</string>
<string name="msg_upload_file">Téléverser un fichier</string>
<string name="msg_file_description">Description du fichier</string>
<string name="msg_send">envoyer</string>
// TODO: Add proper translation. // TODO: Add proper translation.
<string name="msg_delete_message">Delete Message</string> <string name="msg_delete_message">Delete Message</string>
<string name="msg_delete_description">Are you sure you want to delete this message</string> <string name="msg_delete_description">Are you sure you want to delete this message</string>
...@@ -283,4 +286,5 @@ ...@@ -283,4 +286,5 @@
<string name="notif_action_reply_hint">RÉPONDRE</string> <string name="notif_action_reply_hint">RÉPONDRE</string>
<string name="notif_error_sending">La réponse a échoué. Veuillez réessayer.</string> <string name="notif_error_sending">La réponse a échoué. Veuillez réessayer.</string>
<string name="notif_success_sending">Message envoyé à %1$s!</string> <string name="notif_success_sending">Message envoyé à %1$s!</string>
<string name="msg_log_out">Logging out...</string>
</resources> </resources>
...@@ -15,8 +15,7 @@ ...@@ -15,8 +15,7 @@
<string name="title_password">पासवर्ड बदलें</string> <string name="title_password">पासवर्ड बदलें</string>
<string name="title_update_profile">प्रोफ़ाइल अपडेट करें</string> <string name="title_update_profile">प्रोफ़ाइल अपडेट करें</string>
<string name="title_about">परिचय</string> <string name="title_about">परिचय</string>
// TODO: Add proper translation. <string name="title_create_channel">चैनल बनाएं</string>
<string name="title_create_channel">Create Channel</string>
<!-- Actions --> <!-- Actions -->
<string name="action_connect">जुडिये</string> <string name="action_connect">जुडिये</string>
...@@ -27,9 +26,8 @@ ...@@ -27,9 +26,8 @@
<string name="action_search">खोजें</string> <string name="action_search">खोजें</string>
<string name="action_update">अद्यतन करें</string> <string name="action_update">अद्यतन करें</string>
<string name="action_settings">सेटिंग्स</string> <string name="action_settings">सेटिंग्स</string>
// TODO: Add proper translation. <string name="action_create_channel">चैनल बनाएं</string>
<string name="action_create_channel">Create channel</string> <string name="action_create">बनाएं</string>
<string name="action_create">Create</string>
<string name="action_logout">लोग आउट करें</string> <string name="action_logout">लोग आउट करें</string>
<string name="action_files">फ़ाइलें</string> <string name="action_files">फ़ाइलें</string>
<string name="action_confirm_password">पासवर्ड परिवर्तन की पुष्टि करें</string> <string name="action_confirm_password">पासवर्ड परिवर्तन की पुष्टि करें</string>
...@@ -90,7 +88,7 @@ ...@@ -90,7 +88,7 @@
<string name="msg_preview_video">वीडियो</string> <string name="msg_preview_video">वीडियो</string>
<string name="msg_preview_audio">ऑडियो</string> <string name="msg_preview_audio">ऑडियो</string>
<string name="msg_preview_photo">तस्वीरें</string> <string name="msg_preview_photo">तस्वीरें</string>
<string name="msg_preview_file">File</string> <string name="msg_preview_file">फ़ाइल</string>
<string name="msg_unread_messages">अपठित संदेश</string> <string name="msg_unread_messages">अपठित संदेश</string>
<string name="msg_no_messages_yet">अभी तक कोई पोस्ट नहीं</string> <string name="msg_no_messages_yet">अभी तक कोई पोस्ट नहीं</string>
<string name="msg_version">वर्शन %1$s</string> <string name="msg_version">वर्शन %1$s</string>
...@@ -119,26 +117,26 @@ ...@@ -119,26 +117,26 @@
<string name="msg_are_typing">\u0020टाइप कर रहे हैं…</string> <string name="msg_are_typing">\u0020टाइप कर रहे हैं…</string>
<string name="msg_several_users_are_typing">कई उपयोगकर्ता टाइप कर रहे हैं…</string> <string name="msg_several_users_are_typing">कई उपयोगकर्ता टाइप कर रहे हैं…</string>
<string name="msg_no_search_found">कोई परिणाम नहीं मिला</string> <string name="msg_no_search_found">कोई परिणाम नहीं मिला</string>
// TODO: Add proper translation. <string name="msg_channel_name">चैनल का नाम</string>
<string name="msg_channel_name">Channel name</string> <string name="msg_search">खोजें</string>
// TODO: Add proper translation.
<string name="msg_search">Search</string>
<string name="msg_message_copied">संदेश कॉपी किया गया</string> <string name="msg_message_copied">संदेश कॉपी किया गया</string>
<string name="msg_upload_file">फाइल अपलोड करें</string>
<string name="msg_file_description">फाइल विवरण</string>
<string name="msg_send">भेजें</string>
<string name="msg_delete_message">संदेश को हटाएं</string> <string name="msg_delete_message">संदेश को हटाएं</string>
<string name="msg_delete_description">क्या आप निश्चित रूप से यह संदेश हटाना चाहते हैं</string> <string name="msg_delete_description">क्या आप निश्चित रूप से यह संदेश हटाना चाहते हैं</string>
<!-- Create channel messages --> <!-- Create channel messages -->
// TODO: Add proper translation. <string name="msg_private_channel">प्राइवेट</string>
<string name="msg_private_channel">Private</string> <string name="msg_public_channel">सार्वजनिक</string>
<string name="msg_public_channel">Public</string> <string name="msg_private_channel_description">केवल आप और आमंत्रित सदस्य ही इस चैनल तक पहुंच सकते हैं</string>
<string name="msg_private_channel_description">Only you and invited members can access this channel</string> <string name="msg_public_channel_description">हर कोई इस चैनल तक पहुंच सकता है</string>
<string name="msg_public_channel_description">Everyone can access this channel</string> <string name="msg_ready_only_channel">चैनल केवल पढ़ने के लिए है।</string>
<string name="msg_ready_only_channel">Read only channel</string> <string name="msg_ready_only_channel_description">केवल एडमिन नए संदेश लिख सकते हैं</string>
<string name="msg_ready_only_channel_description">Only admin can write new messages</string> <string name="msg_invite_members">सदस्यों को चैनल में आमंत्रित करें</string>
<string name="msg_invite_members">Invite members to channel</string> <string name="msg_member_already_added">आपने पहले से ही इस यूजर को चुन चुके है।</string>
<string name="msg_member_already_added">You have already selected this user</string> <string name="msg_member_not_found">सदस्य नहीं मिला</string>
<string name="msg_member_not_found">Member not found</string> <string name="msg_channel_created_successfully">चैनल सफलतापूर्वक बनाया गया</string>
<string name="msg_channel_created_successfully">Channel created successfully</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">%2$s ने रूम का नाम बदलकर %1$s किया</string> <string name="message_room_name_changed">%2$s ने रूम का नाम बदलकर %1$s किया</string>
...@@ -177,18 +175,16 @@ ...@@ -177,18 +175,16 @@
<string name="permission_starring_not_allowed">तारांकित की अनुमति नहीं है</string> <string name="permission_starring_not_allowed">तारांकित की अनुमति नहीं है</string>
<!-- Favorite/Unfavorite chat room --> <!-- Favorite/Unfavorite chat room -->
<!-- TODO Add proper translation--> <string name="title_favorite_chat">पसंदीदा चैट</string>
<string name="title_favorite_chat">Favorite chat</string> <string name="title_unfavorite_chat">नापसंद चैट</string>
<string name="title_unfavorite_chat">Unfavorite chat</string>
<!-- Members List --> <!-- Members List -->
<string name="title_members_list">सदस्य</string> <string name="title_members_list">सदस्य</string>
<!-- Mentions --> <!-- Mentions -->
<!-- TODO Add proper translation--> <string name="msg_mentions">ज़िक्र</string>
<string name="msg_mentions">Mentions</string> <string name="msg_no_mention">कोई ज़िक्र नहीं</string>
<string name="msg_no_mention">No mention</string> <string name="msg_all_the_mentions_appear_here">सभी ज़िक्र यहां दिखाई देते हैं</string>
<string name="msg_all_the_mentions_appear_here">All the mentions\nappear here</string>
<!-- Pinned Messages --> <!-- Pinned Messages -->
<string name="title_pinned_messages">पिन किए गए संदेश</string> <string name="title_pinned_messages">पिन किए गए संदेश</string>
...@@ -234,7 +230,7 @@ ...@@ -234,7 +230,7 @@
<string name="Archive">संग्रहित करें</string> <string name="Archive">संग्रहित करें</string>
<string name="Remove_someone_from_room">रूम से किसी को निकालें</string> <string name="Remove_someone_from_room">रूम से किसी को निकालें</string>
<string name="Leave_the_current_channel">मौजूदा चैनल को छोड़ दें</string> <string name="Leave_the_current_channel">मौजूदा चैनल को छोड़ दें</string>
<string name="Displays_action_text">Displays action text</string> <string name="Displays_action_text">कार्रवाई पाठ प्रदर्शित करता है</string>
<string name="Direct_message_someone">किसी को प्रत्यक्ष संदेश भेजें</string> <string name="Direct_message_someone">किसी को प्रत्यक्ष संदेश भेजें</string>
<string name="Mute_someone_in_room">रूम में किसी को म्यूट करें</string> <string name="Mute_someone_in_room">रूम में किसी को म्यूट करें</string>
<string name="Unmute_someone_in_room">रूम में किसी को अनम्यूट करें</string> <string name="Unmute_someone_in_room">रूम में किसी को अनम्यूट करें</string>
...@@ -260,11 +256,12 @@ ...@@ -260,11 +256,12 @@
<string name="header_channel">चैनलों</string> <string name="header_channel">चैनलों</string>
<string name="header_private_groups">निजी समूहों</string> <string name="header_private_groups">निजी समूहों</string>
<string name="header_direct_messages">प्रत्यक्ष संदेश</string> <string name="header_direct_messages">प्रत्यक्ष संदेश</string>
<string name="header_live_chats">Live Chats</string> <string name="header_live_chats">लाइव चैट</string>
<string name="header_unknown">अज्ञात</string> <string name="header_unknown">अज्ञात</string>
<!--Notifications--> <!--Notifications-->
<string name="notif_action_reply_hint">जवाब</string> <string name="notif_action_reply_hint">जवाब</string>
<string name="notif_error_sending">उत्तर विफल हुआ है। कृपया फिर से प्रयास करें।</string> <string name="notif_error_sending">उत्तर विफल हुआ है। कृपया फिर से प्रयास करें।</string>
<string name="notif_success_sending">संदेश भेजा गया %1$s!</string> <string name="notif_success_sending">संदेश भेजा गया %1$s!</string>
<string name="msg_log_out">Logging out...</string>
</resources> </resources>
\ No newline at end of file
...@@ -120,6 +120,9 @@ ...@@ -120,6 +120,9 @@
<string name="msg_channel_name">Nome do chat</string> <string name="msg_channel_name">Nome do chat</string>
<string name="msg_search">Buscar</string> <string name="msg_search">Buscar</string>
<string name="msg_message_copied">Mensagem copiada</string> <string name="msg_message_copied">Mensagem copiada</string>
<string name="msg_upload_file">Subir arquivo</string>
<string name="msg_file_description">Descrição do arquivo</string>
<string name="msg_send">Enviar</string>
// TODO: Add proper translation. // TODO: Add proper translation.
<string name="msg_delete_message">Delete Message</string> <string name="msg_delete_message">Delete Message</string>
<string name="msg_delete_description">Are you sure you want to delete this message</string> <string name="msg_delete_description">Are you sure you want to delete this message</string>
...@@ -262,4 +265,5 @@ ...@@ -262,4 +265,5 @@
<string name="notif_action_reply_hint">RESPONDER</string> <string name="notif_action_reply_hint">RESPONDER</string>
<string name="notif_error_sending">Falha ao enviar a mensagem.</string> <string name="notif_error_sending">Falha ao enviar a mensagem.</string>
<string name="notif_success_sending">Mensagem enviada para %1$s!</string> <string name="notif_success_sending">Mensagem enviada para %1$s!</string>
<string name="msg_log_out">Deslogando...</string>
</resources> </resources>
...@@ -116,6 +116,9 @@ ...@@ -116,6 +116,9 @@
<string name="msg_several_users_are_typing">Несколько пользователей печатают…</string> <string name="msg_several_users_are_typing">Несколько пользователей печатают…</string>
<string name="msg_no_search_found">Результатов не найдено</string> <string name="msg_no_search_found">Результатов не найдено</string>
<string name="msg_message_copied">Сообщение скопировано</string> <string name="msg_message_copied">Сообщение скопировано</string>
<string name="msg_upload_file">Загрузить файл</string>
<string name="msg_file_description">Описание файла</string>
<string name="msg_send">послать</string>
<string name="msg_delete_message">Удалить сообщение</string> <string name="msg_delete_message">Удалить сообщение</string>
<string name="msg_delete_description">Вы уверены, что хотите удалить это сообщение?</string> <string name="msg_delete_description">Вы уверены, что хотите удалить это сообщение?</string>
<string name="msg_channel_name">Название канала</string> <string name="msg_channel_name">Название канала</string>
...@@ -257,4 +260,5 @@ ...@@ -257,4 +260,5 @@
<string name="notif_action_reply_hint">ОТВЕТИТЬ</string> <string name="notif_action_reply_hint">ОТВЕТИТЬ</string>
<string name="notif_error_sending">Ошибка ответа. Пожалуйста, попробуйте еще раз.</string> <string name="notif_error_sending">Ошибка ответа. Пожалуйста, попробуйте еще раз.</string>
<string name="notif_success_sending">Сообщение отправлено %1$s!</string> <string name="notif_success_sending">Сообщение отправлено %1$s!</string>
<string name="msg_log_out">Logging out...</string>
</resources> </resources>
...@@ -120,6 +120,10 @@ ...@@ -120,6 +120,10 @@
<string name="msg_are_typing">\u0020are 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_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>
<string name="msg_log_out">Logging out...</string>
<string name="msg_upload_file">Upload file</string>
<string name="msg_file_description">File description</string>
<string name="msg_send">Send</string>
<!-- Create channel messages --> <!-- Create channel messages -->
<string name="msg_private_channel">Private</string> <string name="msg_private_channel">Private</string>
...@@ -132,7 +136,6 @@ ...@@ -132,7 +136,6 @@
<string name="msg_member_already_added">You have already selected this user</string> <string name="msg_member_already_added">You have already selected this user</string>
<string name="msg_member_not_found">Member not found</string> <string name="msg_member_not_found">Member not found</string>
<string name="msg_channel_created_successfully">Channel created successfully</string> <string name="msg_channel_created_successfully">Channel created successfully</string>
<string name="msg_message_copied">Message copied</string> <string name="msg_message_copied">Message copied</string>
<string name="msg_delete_message">Delete Message</string> <string name="msg_delete_message">Delete Message</string>
<string name="msg_delete_description">Are you sure you want to delete this message</string> <string name="msg_delete_description">Are you sure you want to delete this message</string>
......
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