Commit 35285092 authored by Lucio Maciel's avatar Lucio Maciel

Room database on Chat Rooms screen.

parent 54918bf1
......@@ -107,6 +107,8 @@ dependencies {
implementation libraries.aVLoadingIndicatorView
implementation "com.github.luciofm:livedata-ktx:b1e8bbc25a"
implementation('com.crashlytics.sdk.android:crashlytics:2.9.2@aar') {
transitive = true
}
......
......@@ -577,7 +577,7 @@ class ChatRoomPresenter @Inject constructor(
try {
val chatRooms = getChatRoomsInteractor.getAll(currentServer)
.filterNot {
it.type is RoomType.DirectMessage || it.type is RoomType.Livechat
it.type is RoomType.DirectMessage || it.type is RoomType.LiveChat
}
.map { chatRoom ->
val name = chatRoom.name
......
......@@ -595,7 +595,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
recycler_view.itemAnimator = DefaultItemAnimator()
endlessRecyclerViewScrollListener = object :
EndlessRecyclerViewScrollListener(recycler_view.layoutManager as LinearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView) {
presenter.loadMessages(chatRoomId, chatRoomType, page * 30L)
}
}
......
package chat.rocket.android.chatrooms.adapter
data class HeaderItemHolder(override val data: String) : ItemHolder<String>
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter
import android.view.View
import kotlinx.android.synthetic.main.item_chatroom_header.view.*
class HeaderViewHolder(itemView: View) : ViewHolder<HeaderItemHolder>(itemView) {
override fun bindViews(data: HeaderItemHolder) {
with(itemView) {
text_chatroom_header.text = data.data
}
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter
interface ItemHolder<T> {
val data: T
}
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter
import chat.rocket.android.chatrooms.adapter.model.Room
data class RoomItemHolder(override val data: Room) : ItemHolder<Room>
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter
import android.app.Application
import android.text.SpannableStringBuilder
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.color
import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.Room
import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.checkIfMyself
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.showLastMessage
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.date
import chat.rocket.android.util.extensions.localDateTime
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.model.userStatusOf
class RoomMapper(private val context: Application,
private val settings: PublicSettings,
private val localRepository: LocalRepository,
private val serverUrl: String) {
private val nameUnreadColor = ContextCompat.getColor(context, R.color.colorPrimaryText)
private val nameColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
private val dateUnreadColor = ContextCompat.getColor(context, R.color.colorAccent)
private val dateColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
private val messageUnreadColor = ContextCompat.getColor(context, android.R.color.primary_text_light)
private val messageColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
fun map(rooms: List<ChatRoom>, grouped: Boolean): List<ItemHolder<*>> {
val list = ArrayList<ItemHolder<*>>(rooms.size + 4)
var lastType: String? = null
rooms.forEach { room ->
if (grouped && lastType != room.chatRoom.type) {
list.add(HeaderItemHolder(roomType(room.chatRoom.type)))
}
list.add(RoomItemHolder(map(room)))
lastType = room.chatRoom.type
}
return list
}
fun map(chatRoom: ChatRoom): Room {
return with(chatRoom.chatRoom) {
val isUnread = alert || unread > 0
val type = roomTypeOf(type)
val status = chatRoom.status?.let { userStatusOf(it) }
val roomName = mapName(name, chatRoom.userFullname, isUnread)
val timestamp = mapDate(lastMessageTimestamp ?: updatedAt, isUnread)
val avatar = if (type is RoomType.DirectMessage) {
serverUrl.avatarUrl(name)
} else {
serverUrl.avatarUrl(name, isGroupOrChannel = true)
}
val unread = mapUnread(unread)
val lastMessage = mapLastMessage(chatRoom.lastMessageUserName,
chatRoom.lastMessageUserFullName, lastMessageText, isUnread)
Room(
id = id,
name = roomName,
type = type,
avatar = avatar,
date = timestamp,
unread = unread,
alert = isUnread,
lastMessage = lastMessage,
status = status
)
}
}
private fun roomType(type: String): String {
val resources = context.resources
return when (type) {
RoomType.CHANNEL -> resources.getString(R.string.header_channel)
RoomType.PRIVATE_GROUP -> resources.getString(R.string.header_private_groups)
RoomType.DIRECT_MESSAGE -> resources.getString(R.string.header_direct_messages)
RoomType.LIVECHAT -> resources.getString(R.string.header_live_chats)
else -> resources.getString(R.string.header_unknown)
}
}
private fun mapLastMessage(name: String?, fullName: String?, text: String?, unread: Boolean): CharSequence? {
return if (!settings.showLastMessage()) {
null
} else if (name != null && fullName != null && text != null) {
val user = if (localRepository.checkIfMyself(name)) {
"${context.getString(R.string.msg_you)}: "
} else {
"${mapName(name, fullName, unread)}: "
}
val color = if (unread) messageUnreadColor else messageColor
SpannableStringBuilder()
.color(color) {
bold { append(user) }
append(text)
}
} else {
context.getText(R.string.msg_no_messages_yet)
}
}
private fun mapName(name: String, fullName: String?, unread: Boolean): CharSequence {
val roomName = if (settings.useRealName()) {
fullName ?: name
} else {
name
}
val color = if (unread) nameUnreadColor else nameColor
return SpannableStringBuilder()
.color(color) {
append(roomName)
}
}
private fun mapUnread(unread: Long): String? {
return when(unread) {
0L -> null
in 1..99 -> unread.toString()
else -> context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
}
}
private fun mapDate(date: Long?, unread: Boolean): CharSequence? {
return date?.localDateTime()?.date(context)?.let {
val color = if (unread) dateUnreadColor else dateColor
SpannableStringBuilder().color(color) {
append(it)
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.UserStatus
import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class RoomViewHolder(itemView: View) : ViewHolder<RoomItemHolder>(itemView) {
private val resources: Resources = itemView.resources
private val channelUnread: Drawable = resources.getDrawable(R.drawable.ic_hashtag_unread_12dp)
private val channel: Drawable = resources.getDrawable(R.drawable.ic_hashtag_12dp)
private val groupUnread: Drawable = resources.getDrawable(R.drawable.ic_lock_unread_12_dp)
private val group: Drawable = resources.getDrawable(R.drawable.ic_lock_12_dp)
private val online: Drawable = resources.getDrawable(R.drawable.ic_status_online_12dp)
private val away: Drawable = resources.getDrawable(R.drawable.ic_status_away_12dp)
private val busy: Drawable = resources.getDrawable(R.drawable.ic_status_busy_12dp)
private val offline: Drawable = resources.getDrawable(R.drawable.ic_status_invisible_12dp)
override fun bindViews(data: RoomItemHolder) {
val room = data.data
with(itemView) {
image_avatar.setImageURI(room.avatar)
text_chat_name.text = room.name
if (room.lastMessage != null) {
text_last_message.isVisible = true
text_last_message.text = room.lastMessage
} else {
text_last_message.isGone = true
}
if (room.date != null) {
text_last_message_date_time.isVisible = true
text_last_message_date_time.text = room.date
} else {
text_last_message_date_time.isGone = true
}
if (room.unread != null) {
text_total_unread_messages.isVisible = true
text_total_unread_messages.text = room.unread
} else {
text_total_unread_messages.isGone = true
}
if (room.status != null && room.type is RoomType.DirectMessage) {
image_chat_icon.setImageDrawable(getStatusDrawable(room.status))
} else {
image_chat_icon.setImageDrawable(getRoomDrawable(room.type, room.alert))
}
}
}
private fun getRoomDrawable(type: RoomType, alert: Boolean): Drawable? {
return when(type) {
is RoomType.Channel -> if (alert) channelUnread else channel
is RoomType.PrivateGroup -> if (alert) groupUnread else group
else -> null
}
}
private fun getStatusDrawable(status: UserStatus): Drawable {
return when(status) {
is UserStatus.Online -> online
is UserStatus.Away -> away
is UserStatus.Busy -> busy
else -> offline
}
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.util.extensions.inflate
class RoomsAdapter : RecyclerView.Adapter<ViewHolder<*>>() {
init {
setHasStableIds(true)
}
var values: List<ItemHolder<*>> = ArrayList(0)
set(items) {
field = items
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> {
if (viewType == 0) {
val view = parent.inflate(R.layout.item_chat)
return RoomViewHolder(view)
} else if (viewType == 1) {
val view = parent.inflate(R.layout.item_chatroom_header)
return HeaderViewHolder(view)
}
throw IllegalStateException("View type must be either Room or Header")
}
override fun getItemCount() = values.size
override fun getItemId(position: Int): Long {
val item = values[position]
return when(item) {
is HeaderItemHolder -> item.data.hashCode().toLong()
is RoomItemHolder -> item.data.id.hashCode().toLong()
else -> throw IllegalStateException("View type must be either Room or Header")
}
}
override fun getItemViewType(position: Int): Int {
return when(values[position]) {
is RoomItemHolder -> 0
is HeaderItemHolder -> 1
else -> throw IllegalStateException("View type must be either Room or Header")
}
}
override fun onBindViewHolder(holder: ViewHolder<*>, position: Int) {
if (holder is RoomViewHolder) {
holder.bind(values[position] as RoomItemHolder)
} else if (holder is HeaderViewHolder) {
holder.bind(values[position] as HeaderItemHolder)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter
import android.view.View
import androidx.recyclerview.widget.RecyclerView
abstract class ViewHolder<T : ItemHolder<*>>(
itemView: View
) : RecyclerView.ViewHolder(itemView) {
var data: T? = null
fun bind(data: T) {
this.data = data
bindViews(data)
}
abstract fun bindViews(data: T)
}
\ No newline at end of file
package chat.rocket.android.chatrooms.adapter.model
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.UserStatus
data class Room(
val id: String,
val type: RoomType,
val name: CharSequence,
val avatar: String,
val date: CharSequence?,
val unread: String?,
val alert: Boolean,
val lastMessage: CharSequence?,
val status: UserStatus?
)
\ No newline at end of file
package chat.rocket.android.chatrooms.di
import android.app.Application
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.chatrooms.adapter.RoomMapper
import chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.core.RocketChatClient
import dagger.Module
import dagger.Provides
import javax.inject.Named
@Module
@PerFragment
class ChatRoomsFragmentModule {
@Provides
@PerFragment
fun chatRoomsView(frag: ChatRoomsFragment): ChatRoomsView {
return frag
}
@Provides
@PerFragment
fun provideLifecycleOwner(frag: ChatRoomsFragment): LifecycleOwner {
return frag
}
@Provides
@PerFragment
@Named("currentServer")
fun provideCurrentServer(currentServerInteractor: GetCurrentServerInteractor): String {
return currentServerInteractor.get()!!
}
@Provides
@PerFragment
fun provideRocketChatClient(factory: RocketChatClientFactory,
@Named("currentServer") currentServer: String): RocketChatClient {
return factory.create(currentServer)
}
@Provides
@PerFragment
fun provideDatabaseManager(factory: DatabaseManagerFactory,
@Named("currentServer") currentServer: String): DatabaseManager {
return factory.create(currentServer)
}
@Provides
@PerFragment
fun provideChatRoomDao(manager: DatabaseManager): ChatRoomDao = manager.chatRoomDao()
@Provides
@PerFragment
fun provideConnectionManager(factory: ConnectionManagerFactory,
@Named("currentServer") currentServer: String): ConnectionManager {
return factory.create(currentServer)
}
@Provides
@PerFragment
fun provideFetchChatRoomsInteractor(client: RocketChatClient, dbManager: DatabaseManager): FetchChatRoomsInteractor {
return FetchChatRoomsInteractor(client, dbManager)
}
@Provides
@PerFragment
fun providePublicSettings(repository: SettingsRepository,
@Named("currentServer") currentServer: String): PublicSettings {
return repository.get(currentServer)
}
@Provides
@PerFragment
fun provideRoomMapper(context: Application,
repository: SettingsRepository,
localRepository: LocalRepository,
@Named("currentServer") serverUrl: String): RoomMapper {
return RoomMapper(context, repository.get(serverUrl), localRepository, serverUrl)
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.domain
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.util.retryIO
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.userId
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
class FetchChatRoomsInteractor(
private val client: RocketChatClient,
private val dbManager: DatabaseManager
) {
suspend fun refreshChatRooms() {
launch(CommonPool) {
try {
val rooms = retryIO("fetch chatRooms", times = 10,
initialDelay = 200, maxDelay = 2000) {
client.chatRooms().update.map { room ->
mapChatRoom(room)
}
}
Timber.d("Refreshing rooms: $rooms")
dbManager.insert(rooms)
} catch (ex: Exception) {
Timber.d(ex, "Error getting chatrooms")
}
}
}
private suspend fun mapChatRoom(room: ChatRoom): ChatRoomEntity {
with(room) {
val userId = userId()
if (userId != null && dbManager.findUser(userId) == null) {
Timber.d("Missing user, inserting: $userId")
dbManager.insert(UserEntity(userId))
}
lastMessage?.sender?.let { user ->
if (dbManager.findUser(user.id!!) == null) {
Timber.d("Missing last message user, inserting: ${user.id}")
dbManager.insert(UserEntity(user.id!!, user.username, user.name))
}
}
return ChatRoomEntity(
id = id,
subscriptionId = subscriptionId,
type = type.toString(),
name = name,
userId = userId,
readonly = readonly,
isDefault = default,
favorite = favorite,
open = open,
alert = alert,
unread = unread,
userMentions = userMentions,
groupMentions = groupMentions,
updatedAt = updatedAt,
timestamp = timestamp,
lastSeen = lastSeen,
lastMessageText = lastMessage?.message,
lastMessageUserId = lastMessage?.sender?.id,
lastMessageTimestamp = lastMessage?.timestamp
)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.infrastructure
import androidx.lifecycle.LiveData
import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.model.ChatRoom
import javax.inject.Inject
class ChatRoomsRepository @Inject constructor(val dao: ChatRoomDao){
fun getChatRooms(order: Order): LiveData<List<ChatRoom>> {
return when(order) {
Order.ACTIVITY -> dao.getAll()
Order.GROUPED_ACTIVITY -> dao.getAllGrouped()
Order.NAME -> dao.getAllAlphabetically()
Order.GROUPED_NAME -> dao.getAllAlphabeticallyGrouped()
}
}
fun fetchChatRooms() {
}
enum class Order {
ACTIVITY,
GROUPED_ACTIVITY,
NAME,
GROUPED_NAME,
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants
......@@ -9,9 +10,18 @@ import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.GetActiveUsersInteractor
import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.JobSchedulerInteractor
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveActiveUsersInteractor
import chat.rocket.android.server.domain.SaveChatRoomsInteractor
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.hasShowLastMessage
import chat.rocket.android.server.domain.showLastMessage
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.chatRooms
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI
......@@ -21,6 +31,7 @@ import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.SimpleUser
import chat.rocket.common.model.User
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.socket.model.State
......@@ -31,18 +42,24 @@ import chat.rocket.core.internal.rest.permissions
import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Deferred
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Named
import kotlin.reflect.KProperty1
class ChatRoomsPresenter @Inject constructor(
private val view: ChatRoomsView,
private val strategy: CancelStrategy,
private val navigator: MainNavigator,
private val serverInteractor: GetCurrentServerInteractor,
@Named("currentServer") private val currentServer: String,
private val manager: ConnectionManager,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val saveActiveUsersInteractor: SaveActiveUsersInteractor,
......@@ -53,11 +70,9 @@ class ChatRoomsPresenter @Inject constructor(
private val permissionsInteractor: PermissionsInteractor,
private val localRepository: LocalRepository,
private val userHelper: UserHelper,
settingsRepository: SettingsRepository,
factory: ConnectionManagerFactory
private val roomsInteractor: FetchChatRoomsInteractor,
settingsRepository: SettingsRepository
) {
private val manager: ConnectionManager = factory.create(serverInteractor.get()!!)
private val currentServer = serverInteractor.get()!!
private val client = manager.client
private var reloadJob: Deferred<List<ChatRoom>>? = null
private val settings = settingsRepository.get(currentServer)
......@@ -79,6 +94,8 @@ class ChatRoomsPresenter @Inject constructor(
view.updateChatRooms(getUserChatRooms())
val permissions = retryIO { client.permissions() }
permissionsInteractor.saveAll(permissions)
roomsInteractor.refreshChatRooms()
} catch (ex: RocketChatException) {
ex.message?.let {
view.showMessage(it)
......@@ -145,7 +162,6 @@ class ChatRoomsPresenter @Inject constructor(
* ChatRooms returned are filtered by name.
*/
fun chatRoomsByName(name: String) {
val currentServer = serverInteractor.get()!!
launchUI(strategy) {
try {
val roomList = getChatRoomsInteractor.getAllByName(currentServer, name)
......@@ -187,7 +203,8 @@ class ChatRoomsPresenter @Inject constructor(
return users.map {
ChatRoom(
id = it.id,
type = RoomType.DIRECT_MESSAGE,
subscriptionId = it.id,
type = roomTypeOf(RoomType.DIRECT_MESSAGE),
user = SimpleUser(username = it.username, name = it.name, id = null),
status = if (it.name != null) {
getActiveUsersInteractor.getActiveUserByUsername(currentServer, it.name!!)
......@@ -221,6 +238,7 @@ class ChatRoomsPresenter @Inject constructor(
return rooms.map {
ChatRoom(
id = it.id,
subscriptionId = it.id,
type = it.type,
user = it.user,
status = if (it.name != null) {
......@@ -304,7 +322,7 @@ class ChatRoomsPresenter @Inject constructor(
is RoomType.Channel -> Constants.CHATROOM_CHANNEL
is RoomType.PrivateGroup -> Constants.CHATROOM_PRIVATE_GROUP
is RoomType.DirectMessage -> Constants.CHATROOM_DM
is RoomType.Livechat -> Constants.CHATROOM_LIVE_CHAT
is RoomType.LiveChat -> Constants.CHATROOM_LIVE_CHAT
else -> 0
}
}
......@@ -314,6 +332,7 @@ class ChatRoomsPresenter @Inject constructor(
chatRooms.forEach {
val newRoom = ChatRoom(
id = it.id,
subscriptionId = it.id,
type = it.type,
user = it.user,
status = getActiveUsersInteractor.getActiveUserByUsername(
......@@ -458,6 +477,7 @@ class ChatRoomsPresenter @Inject constructor(
chatRoom?.apply {
val newRoom = ChatRoom(
id = room.id,
subscriptionId = this.subscriptionId,
type = room.type,
user = room.user,
status = getActiveUsersInteractor.getActiveUserByUsername(
......@@ -498,6 +518,7 @@ class ChatRoomsPresenter @Inject constructor(
chatRoom?.apply {
val newRoom = ChatRoom(
id = subscription.roomId,
subscriptionId = subscription.id,
type = subscription.type,
user = user,
status = getActiveUsersInteractor.getActiveUserByUsername(
......@@ -576,6 +597,7 @@ class ChatRoomsPresenter @Inject constructor(
getChatRoomsInteractor.getByName(currentServer, username)?.let {
val newRoom = ChatRoom(
id = it.id,
subscriptionId = it.id,
type = it.type,
user = it.user,
status = status,
......
......@@ -92,7 +92,7 @@ class ChatRoomsAdapter(
private fun bindIcon(chatRoom: ChatRoom, imageView: ImageView) {
val drawable = when (chatRoom.type) {
is RoomType.Channel -> DrawableHelper.getDrawableFromId(
R.drawable.ic_hashtag_12dp,
R.drawable.ic_hashtag_unread_12dp,
context
)
is RoomType.PrivateGroup -> DrawableHelper.getDrawableFromId(
......
package chat.rocket.android.chatrooms.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import chat.rocket.android.chatrooms.adapter.ItemHolder
import chat.rocket.android.chatrooms.adapter.RoomMapper
import chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository
import chat.rocket.android.db.model.ChatRoom
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import me.henrytao.livedataktx.distinct
import me.henrytao.livedataktx.nonNull
import me.henrytao.livedataktx.map
import timber.log.Timber
class ChatRoomsViewModel(
private val interactor: FetchChatRoomsInteractor,
private val repository: ChatRoomsRepository,
private val mapper: RoomMapper
) : ViewModel() {
private val ordering: MutableLiveData<ChatRoomsRepository.Order> = MutableLiveData()
init {
ordering.value = ChatRoomsRepository.Order.ACTIVITY
}
fun getChatRooms(): LiveData<List<ItemHolder<*>>> {
// TODO - add a loading status...
launch { interactor.refreshChatRooms() }
return Transformations.switchMap(ordering) { order ->
Timber.d("Querying rooms for order: $order")
val grouped = order == ChatRoomsRepository.Order.GROUPED_ACTIVITY
|| order == ChatRoomsRepository.Order.GROUPED_NAME
repository.getChatRooms(order).nonNull()
.distinct()
.map { rooms ->
Timber.d("Mapping rooms to items: $rooms")
mapper.map(rooms, grouped)
}
}
}
fun setOrdering(order: ChatRoomsRepository.Order) {
ordering.value = order
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import chat.rocket.android.chatrooms.adapter.RoomMapper
import chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository
import javax.inject.Inject
class ChatRoomsViewModelFactory @Inject constructor(
private val interactor: FetchChatRoomsInteractor,
private val repository: ChatRoomsRepository,
private val mapper: RoomMapper
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>) =
ChatRoomsViewModel(interactor, repository, mapper) as T
}
\ No newline at end of file
......@@ -50,7 +50,7 @@ import javax.inject.Singleton
@Module
class AppModule {
@Provides
/*@Provides
@Singleton
fun provideRocketChatClient(okHttpClient: OkHttpClient, repository: TokenRepository, logger: PlatformLogger): RocketChatClient {
return RocketChatClient.create {
......@@ -61,7 +61,7 @@ class AppModule {
// TODO remove
restUrl = "https://open.rocket.chat"
}
}
}*/
@Provides
fun provideJob(): Job {
......
package chat.rocket.android.db
import androidx.room.Insert
import androidx.room.OnConflictStrategy
interface BaseDao<T> {
@Insert
fun insert(vararg obj: T)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(list: List<T>)
}
\ No newline at end of file
package chat.rocket.android.db
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.common.model.RoomType
@Dao
abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
@Transaction
@Query("""
$BASE_QUERY
ORDER BY
CASE
WHEN lastMessageTimeStamp IS NOT NULL THEN lastMessageTimeStamp
ELSE updatedAt
END DESC
""")
abstract fun getAll(): LiveData<List<ChatRoom>>
@Transaction
@Query("""
$BASE_QUERY
ORDER BY
$TYPE_ORDER,
CASE
WHEN lastMessageTimeStamp IS NOT NULL THEN lastMessageTimeStamp
ELSE updatedAt
END DESC
""")
abstract fun getAllGrouped(): LiveData<List<ChatRoom>>
@Transaction
@Query("""
$BASE_QUERY
ORDER BY name
""")
abstract fun getAllAlphabetically(): LiveData<List<ChatRoom>>
@Transaction
@Query("""
$BASE_QUERY
ORDER BY
$TYPE_ORDER,
name
""")
abstract fun getAllAlphabeticallyGrouped(): LiveData<List<ChatRoom>>
@Query("SELECT * FROM chatrooms WHERE ID = :id")
abstract fun get(id: String): ChatRoom?
@Query("DELETE FROM chatrooms WHERE ID = :id")
abstract fun delete(id: String)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insertOrReplace(chatRooms: List<ChatRoomEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insertOrReplace(chatRoom: ChatRoomEntity)
@Update
abstract fun update(list: List<ChatRoomEntity>)
@Transaction
open fun update(toRemove: List<String>, toInsert: List<ChatRoomEntity>, toUpdate: List<ChatRoomEntity>) {
insertOrReplace(toInsert)
update(toUpdate)
toRemove.forEach { id ->
delete(id)
}
}
companion object {
const val BASE_QUERY = """
SELECT chatrooms.*,
users.username as username,
users.name as userFullname,
users.status,
lmUsers.username as lastMessageUserName,
lmUsers.name as lastMessageUserFullName
FROM chatrooms
LEFT JOIN users ON chatrooms.userId = users.id
LEFT JOIN users AS lmUsers ON chatrooms.lastMessageUserId = lmUsers.id
"""
const val TYPE_ORDER = """
CASE
WHEN type = '${RoomType.CHANNEL}' THEN 1
WHEN type = '${RoomType.PRIVATE_GROUP}' THEN 2
WHEN type = '${RoomType.DIRECT_MESSAGE}' THEN 3
WHEN type = '${RoomType.LIVECHAT}' THEN 4
ELSE 5
END
"""
}
}
\ No newline at end of file
This diff is collapsed.
package chat.rocket.android.db
import android.app.Application
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DatabaseManagerFactory @Inject constructor(private val context: Application) {
private val cache = HashMap<String, DatabaseManager>()
fun create(serverUrl: String): DatabaseManager {
cache[serverUrl]?.let {
Timber.d("Returning cached database for $serverUrl")
return it
}
Timber.d("Returning FRESH database for $serverUrl")
val db = DatabaseManager(context, serverUrl)
cache[serverUrl] = db
return db
}
}
\ No newline at end of file
package chat.rocket.android.db
import androidx.room.Database
import androidx.room.RoomDatabase
import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.android.db.model.UserEntity
@Database(
entities = [UserEntity::class, ChatRoomEntity::class],
version = 1,
exportSchema = true
)
abstract class RCDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun chatRoomDao(): ChatRoomDao
}
\ No newline at end of file
package chat.rocket.android.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import chat.rocket.android.db.model.BaseUserEntity
import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.db.model.UserStatus
import timber.log.Timber
@Dao
abstract class UserDao : BaseDao<UserEntity> {
@Update(onConflict = OnConflictStrategy.IGNORE)
abstract fun update(user: UserEntity): Int
@Query("UPDATE OR IGNORE users set STATUS = :status where ID = :id")
abstract fun update(id: String, status: String): Int
@Query("SELECT id FROM users WHERE ID = :id")
abstract fun findUser(id: String): String?
@Transaction
open fun upsert(user: BaseUserEntity) {
internalUpsert(user)
}
@Transaction
open fun upsert(users: List<BaseUserEntity>) {
users.forEach { internalUpsert(it) }
}
private inline fun internalUpsert(user: BaseUserEntity) {
val count = if (user is UserStatus) {
update(user.id, user.status)
} else {
update(user as UserEntity)
}
if (count == 0 && user is UserEntity) {
Timber.d("missing user, inserting: ${user.id}")
insert(user)
}
}
}
\ No newline at end of file
package chat.rocket.android.db.model
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "chatrooms",
indices = [
Index(value = ["userId"]),
Index(value = ["subscriptionId"], unique = true),
Index(value = ["updatedAt"])
],
foreignKeys = [
ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["userId"]),
ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["lastMessageUserId"])
]
)
data class ChatRoomEntity(
@PrimaryKey var id: String,
var subscriptionId: String,
var type: String,
var name: String,
var userId: String?,
var readonly: Boolean? = false,
var isDefault: Boolean? = false,
var favorite: Boolean? = false,
var open: Boolean = true,
var alert: Boolean = false,
var unread: Long = 0,
var userMentions: Long? = 0,
var groupMentions: Long? = 0,
var updatedAt: Long? = -1,
var timestamp: Long? = -1,
var lastSeen: Long? = -1,
var lastMessageText: String?,
var lastMessageUserId: String?,
var lastMessageTimestamp: Long?
)
data class ChatRoom(
@Embedded var chatRoom: ChatRoomEntity,
var username: String?,
var userFullname: String?,
var status: String?,
var lastMessageUserName: String?,
var lastMessageUserFullName: String?
)
\ No newline at end of file
package chat.rocket.android.db.model
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "users",
indices = [(Index(value = ["username"], unique = true))])
data class UserEntity(
@PrimaryKey override val id: String,
var username: String? = null,
var name: String? = null,
override var status: String = "offline",
var utcOffset: Float? = null
) : BaseUserEntity
data class UserStatus(
override val id: String,
override val status: String
) : BaseUserEntity
interface BaseUserEntity {
val id: String
val status: String
}
\ No newline at end of file
......@@ -21,7 +21,6 @@ interface LocalRepository {
companion object {
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
const val MIGRATION_FINISHED_KEY = "MIGRATION_FINISHED_KEY"
const val TOKEN_KEY = "token_"
const val SETTINGS_KEY = "settings_"
const val PERMISSIONS_KEY = "permissions_"
......
......@@ -76,7 +76,7 @@ class MembersFragment : Fragment(), MembersView {
adapter.prependData(dataSet)
if (dataSet.size >= 59) { // TODO Check why the API retorns the specified count -1
recycler_view.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView) {
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType, page * 60L)
}
})
......
package chat.rocket.android.server.infraestructure
import androidx.lifecycle.MutableLiveData
import chat.rocket.android.db.DatabaseManager
import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.User
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.subscribeSubscriptions
import chat.rocket.core.internal.realtime.subscribeRooms
import chat.rocket.core.internal.realtime.subscribeUserData
import chat.rocket.core.internal.realtime.subscribeActiveUsers
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.realtime.socket.connect
import chat.rocket.core.internal.realtime.socket.disconnect
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.realtime.socket.model.StreamMessage
import chat.rocket.core.internal.realtime.subscribeActiveUsers
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.subscribeRooms
import chat.rocket.core.internal.realtime.subscribeSubscriptions
import chat.rocket.core.internal.realtime.subscribeUserData
import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.channels.SendChannel
import kotlinx.coroutines.experimental.channels.actor
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.selects.select
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.coroutines.experimental.CoroutineContext
import kotlin.math.absoluteValue
class ConnectionManager(internal val client: RocketChatClient) {
class ConnectionManager(
internal val client: RocketChatClient,
private val dbManager: DatabaseManager
) {
private val statusLiveData = MutableLiveData<State>()
private val statusChannelList = CopyOnWriteArrayList<Channel<State>>()
private val statusChannel = Channel<State>(Channel.CONFLATED)
private var connectJob: Job? = null
......@@ -38,6 +51,9 @@ class ConnectionManager(internal val client: RocketChatClient) {
private var userDataId: String? = null
private var activeUserId: String? = null
private val activeUsersContext = newSingleThreadContext("activeUsersContext")
private val roomsContext = newSingleThreadContext("roomsContext")
fun connect() {
if (connectJob?.isActive == true && (state !is State.Disconnected)) {
Timber.d("Already connected, just returning...")
......@@ -80,6 +96,8 @@ class ConnectionManager(internal val client: RocketChatClient) {
}
}
statusLiveData.postValue(status)
for (channel in statusChannelList) {
Timber.d("Sending status: $status to $channel")
channel.offer(status)
......@@ -87,24 +105,45 @@ class ConnectionManager(internal val client: RocketChatClient) {
}
}
var totalBatchedUsers = 0
val userActor = createBatchActor<User>(activeUsersContext, parent = connectJob,
maxSize = 500, maxTime = 1000) { users ->
totalBatchedUsers += users.size
Timber.d("Processing Users batch: ${users.size} - $totalBatchedUsers")
// TODO - move this to an Interactor
dbManager.processUsersBatch(users)
}
val roomsActor = createBatchActor<StreamMessage<BaseRoom>>(roomsContext, parent = connectJob,
maxSize = 10) { batch ->
Timber.d("processing Stream batch: ${batch.size} - $batch")
dbManager.processStreamBatch(batch)
}
// stream-notify-user - ${userId}/rooms-changed
launch(parent = connectJob) {
for (room in client.roomsChannel) {
Timber.d("GOT Room streamed")
roomsActor.send(room)
for (channel in roomAndSubscriptionChannels) {
channel.send(room)
}
}
}
// stream-notify-user - ${userId}/subscriptions-changed
launch(parent = connectJob) {
for (subscription in client.subscriptionsChannel) {
Timber.d("GOT Subscription streamed")
roomsActor.send(subscription)
for (channel in roomAndSubscriptionChannels) {
channel.send(subscription)
}
}
}
// stream-room-messages - $roomId
launch(parent = connectJob) {
for (message in client.messagesChannel) {
Timber.d("Received new Message for room ${message.roomId}")
......@@ -113,18 +152,24 @@ class ConnectionManager(internal val client: RocketChatClient) {
}
}
// userData
launch(parent = connectJob) {
for (myself in client.userDataChannel) {
Timber.d("Got userData")
userActor.send(myself.asUser())
for (channel in userDataChannels) {
channel.send(myself)
}
}
}
var totalUsers = 0
// activeUsers
launch(parent = connectJob) {
for (user in client.activeUsersChannel) {
Timber.d("Got activeUsers")
totalUsers++
//Timber.d("Got activeUsers: $totalUsers")
userActor.send(user)
for (channel in activeUsersChannels) {
channel.send(user)
}
......@@ -196,6 +241,51 @@ class ConnectionManager(internal val client: RocketChatClient) {
id?.let { client.unsubscribe(it) }
}
}
private inline fun <T> createBatchActor(context: CoroutineContext = CommonPool,
parent: Job? = null,
maxSize: Int = 100,
maxTime: Int = 500,
crossinline block: (List<T>) -> Unit): SendChannel<T> {
return actor(context, parent = parent) {
val batch = ArrayList<T>(maxSize)
var deadline = 0L // deadline for sending this batch to callback block
while(true) {
// when deadline is reached or size is exceeded, pass the batch to the callback block
val remainingTime = deadline - System.currentTimeMillis()
if (batch.isNotEmpty() && remainingTime <= 0 || batch.size >= maxSize) {
Timber.d("Processing batch: ${batch.size}")
block(batch.toList())
batch.clear()
continue
}
// wait until items is received or timeout reached
select<Unit> {
// when received -> add to batch
channel.onReceive {
batch.add(it)
//Timber.d("Adding user to batch: ${batch.size}")
// init deadline on first item added to batch
if (batch.size == 1) deadline = System.currentTimeMillis() + maxTime
}
// when timeout is reached just finish select, note: no timeout when batch is empty
if (batch.isNotEmpty()) onTimeout(remainingTime.orZero()) {}
}
if (!isActive) break
}
}
}
}
private fun Myself.asUser(): User {
return User(id, name, username, status, utcOffset, null, roles)
}
private fun Long.orZero(): Long {
return if (this < 0) 0 else this
}
suspend fun ConnectionManager.chatRooms(timestamp: Long = 0, filterCustom: Boolean = true) =
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.db.DatabaseManagerFactory
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ConnectionManagerFactory @Inject constructor(private val factory: RocketChatClientFactory) {
class ConnectionManagerFactory @Inject constructor(
private val factory: RocketChatClientFactory,
private val dbFactory: DatabaseManagerFactory
) {
private val cache = HashMap<String, ConnectionManager>()
fun create(url: String): ConnectionManager {
......@@ -15,7 +19,7 @@ class ConnectionManagerFactory @Inject constructor(private val factory: RocketCh
}
Timber.d("Returning FRESH Manager for: $url")
val manager = ConnectionManager(factory.create(url))
val manager = ConnectionManager(factory.create(url), dbFactory.create(url))
cache[url] = manager
return manager
}
......
package chat.rocket.android.util.extensions
import android.content.Context
import org.threeten.bp.LocalDateTime
fun LocalDateTime?.date(context: Context): String? {
return this?.let {
DateTimeHelper.getDate(it, context)
}
}
\ No newline at end of file
package chat.rocket.android.util.extensions
import org.threeten.bp.LocalDateTime
fun Long?.localDateTime(): LocalDateTime? {
return this?.let {
DateTimeHelper.getLocalDateTime(it)
}
}
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:fillColor="#787878"
android:fillType="evenOdd"
android:pathData="M2.4,0h1.2v12h-1.2z" />
<path
android:fillColor="#787878"
android:fillType="evenOdd"
android:pathData="M0,2.4h12v1.2h-12z" />
<path
android:fillColor="#787878"
android:fillType="evenOdd"
android:pathData="M0,8.4h12v1.2h-12z" />
<path
android:fillColor="#787878"
android:fillType="evenOdd"
android:pathData="M8.4,0h1.2v12h-1.2z" />
</vector>
\ No newline at end of file
......@@ -4,27 +4,19 @@
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:fillColor="#9EA2A8"
android:fillColor="#DE000000"
android:fillType="evenOdd"
android:pathData="M2.4,0h1.2v12h-1.2z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
android:pathData="M2.4,0h1.2v12h-1.2z" />
<path
android:fillColor="#9EA2A8"
android:fillColor="#DE000000"
android:fillType="evenOdd"
android:pathData="M0,2.4h12v1.2h-12z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
android:pathData="M0,2.4h12v1.2h-12z" />
<path
android:fillColor="#9EA2A8"
android:fillColor="#DE000000"
android:fillType="evenOdd"
android:pathData="M0,8.4h12v1.2h-12z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
android:pathData="M0,8.4h12v1.2h-12z" />
<path
android:fillColor="#9EA2A8"
android:fillColor="#DE000000"
android:fillType="evenOdd"
android:pathData="M8.4,0h1.2v12h-1.2z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
android:pathData="M8.4,0h1.2v12h-1.2z" />
</vector>
\ No newline at end of file
......@@ -6,13 +6,11 @@
<path
android:pathData="M1.5,5.5h9v6h-9z"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#9EA2A8"
android:strokeColor="#787878"
android:fillType="evenOdd"/>
<path
android:pathData="M2.5,5.5L9.5,5.5L9.5,4C9.5,2.067 7.933,0.5 6,0.5C4.067,0.5 2.5,2.067 2.5,4L2.5,5.5Z"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#9EA2A8"
android:strokeColor="#787878"
android:fillType="evenOdd"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M1.5,5.5h9v6h-9z"
android:strokeWidth="1"
android:strokeColor="#DE000000"
android:fillType="evenOdd"/>
<path
android:pathData="M2.5,5.5L9.5,5.5L9.5,4C9.5,2.067 7.933,0.5 6,0.5C4.067,0.5 2.5,2.067 2.5,4L2.5,5.5Z"
android:strokeWidth="1"
android:strokeColor="#DE000000"
android:fillType="evenOdd"/>
</vector>
......@@ -27,7 +27,7 @@
app:layout_constraintBottom_toBottomOf="@+id/text_chat_name"
app:layout_constraintStart_toEndOf="@+id/image_avatar"
app:layout_constraintTop_toTopOf="@+id/text_chat_name"
tools:src="@drawable/ic_hashtag_12dp" />
tools:src="@drawable/ic_hashtag_unread_12dp" />
<TextView
......@@ -58,6 +58,7 @@
android:lines="1"
android:maxLines="1"
android:textDirection="locale"
android:textColor="@color/colorSecondaryText"
app:layout_constraintBottom_toTopOf="@+id/text_last_message"
app:layout_constraintEnd_toStartOf="@+id/text_last_message_date_time"
app:layout_constraintStart_toEndOf="@+id/image_chat_icon"
......@@ -76,7 +77,6 @@
android:ellipsize="end"
android:maxLines="2"
android:textDirection="locale"
tools:visibility="visible"
app:layout_constraintEnd_toStartOf="@+id/layout_unread_messages_badge"
app:layout_constraintTop_toTopOf="@+id/text_chat_name"
tools:text="11:45 AM" />
......
......@@ -4,10 +4,10 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<View
<!--<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/darkGray" />
android:background="@color/darkGray" />-->
<TextView
android:id="@+id/text_chatroom_header"
......@@ -22,6 +22,6 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/darkGray" />
android:background="@color/quoteBar" />
</LinearLayout>
\ No newline at end of file
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