Unverified Commit 7dcb3b46 authored by Lucio Maciel's avatar Lucio Maciel Committed by GitHub

Merge branch 'release/2.0.0-beta7' into feature/member-details

parents 13161ca1 1cb3f896
...@@ -12,8 +12,8 @@ android { ...@@ -12,8 +12,8 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 1009 versionCode 1010
versionName "2.0.0-dev7" versionName "2.0.0-dev8"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
} }
...@@ -60,6 +60,8 @@ dependencies { ...@@ -60,6 +60,8 @@ dependencies {
implementation libraries.constraintLayout implementation libraries.constraintLayout
implementation libraries.cardView implementation libraries.cardView
implementation libraries.androidKtx
implementation libraries.dagger implementation libraries.dagger
implementation libraries.daggerSupport implementation libraries.daggerSupport
kapt libraries.daggerProcessor kapt libraries.daggerProcessor
......
...@@ -6,11 +6,15 @@ import chat.rocket.android.player.PlayerActivity ...@@ -6,11 +6,15 @@ import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import kotlinx.android.synthetic.main.message_attachment.view.* import kotlinx.android.synthetic.main.message_attachment.view.*
class AudioAttachmentViewHolder(itemView: View) : BaseViewHolder<AudioAttachmentViewModel>(itemView) { class AudioAttachmentViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<AudioAttachmentViewModel>(itemView, listener) {
init { init {
with(itemView) { with(itemView) {
image_attachment.setVisible(false) image_attachment.setVisible(false)
audio_video_attachment.setVisible(true) audio_video_attachment.setVisible(true)
setupActionMenu(attachment_container)
setupActionMenu(audio_video_attachment)
} }
} }
......
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.MenuItem
import android.view.View import android.view.View
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
abstract class BaseViewHolder<T>(itemView: View) : RecyclerView.ViewHolder(itemView) { abstract class BaseViewHolder<T : BaseViewModel<*>>(
itemView: View,
private val listener: ActionsListener
) : RecyclerView.ViewHolder(itemView),
MenuItem.OnMenuItemClickListener {
var data: T? = null var data: T? = null
init {
setupActionMenu(itemView)
}
fun bind(data: T) { fun bind(data: T) {
this.data = data this.data = data
bindViews(data) bindViews(data)
} }
abstract fun bindViews(data: T) abstract fun bindViews(data: T)
interface ActionsListener {
fun isActionsEnabled(): Boolean
fun onActionSelected(item: MenuItem, message: Message)
}
val longClickListener = { view: View ->
if (data?.message?.isSystemMessage() == false) {
val menuItems = view.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = data?.message?.pinned ?: false
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = isPinned
}
val adapter = ActionListAdapter(menuItems, this@BaseViewHolder)
BottomSheetMenu(adapter).show(view.context)
}
true
}
internal fun setupActionMenu(view: View) {
if (listener.isActionsEnabled()) {
view.setOnLongClickListener(longClickListener)
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
data?.let {
listener.onActionSelected(item, it.message)
}
return true
}
} }
\ No newline at end of file
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.MenuItem
import android.view.ViewGroup import android.view.ViewGroup
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.viewmodel.* import chat.rocket.android.chatroom.viewmodel.*
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.core.model.Message
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException import java.security.InvalidParameterException
...@@ -26,23 +28,23 @@ class ChatRoomAdapter( ...@@ -26,23 +28,23 @@ class ChatRoomAdapter(
return when(viewType.toViewType()) { return when(viewType.toViewType()) {
BaseViewModel.ViewType.MESSAGE -> { BaseViewModel.ViewType.MESSAGE -> {
val view = parent.inflate(R.layout.item_message) val view = parent.inflate(R.layout.item_message)
MessageViewHolder(view, roomName, roomType, presenter, enableActions) MessageViewHolder(view, actionsListener)
} }
BaseViewModel.ViewType.IMAGE_ATTACHMENT -> { BaseViewModel.ViewType.IMAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment) val view = parent.inflate(R.layout.message_attachment)
ImageAttachmentViewHolder(view) ImageAttachmentViewHolder(view, actionsListener)
} }
BaseViewModel.ViewType.AUDIO_ATTACHMENT -> { BaseViewModel.ViewType.AUDIO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment) val view = parent.inflate(R.layout.message_attachment)
AudioAttachmentViewHolder(view) AudioAttachmentViewHolder(view, actionsListener)
} }
BaseViewModel.ViewType.VIDEO_ATTACHMENT -> { BaseViewModel.ViewType.VIDEO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment) val view = parent.inflate(R.layout.message_attachment)
VideoAttachmentViewHolder(view) VideoAttachmentViewHolder(view, actionsListener)
} }
BaseViewModel.ViewType.URL_PREVIEW -> { BaseViewModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview) val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view) UrlPreviewViewHolder(view, actionsListener)
} }
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
...@@ -108,4 +110,30 @@ class ChatRoomAdapter( ...@@ -108,4 +110,30 @@ class ChatRoomAdapter(
notifyItemRangeRemoved(index, oldSize - newSize) notifyItemRangeRemoved(index, oldSize - newSize)
} }
} }
val actionsListener = object : BaseViewHolder.ActionsListener {
override fun isActionsEnabled(): Boolean = enableActions
override fun onActionSelected(item: MenuItem, message: Message) {
message.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter?.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter?.citeMessage(roomType, roomName, id, false)
R.id.action_menu_msg_reply -> presenter?.citeMessage(roomType, roomName, id, true)
R.id.action_menu_msg_copy -> presenter?.copyMessage(id)
R.id.action_menu_msg_edit -> presenter?.editMessage(roomId, id, message.message)
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
presenter?.pinMessage(id)
} else {
presenter?.unpinMessage(id)
}
}
}
else -> TODO("Not implemented")
}
}
}
}
} }
\ No newline at end of file
...@@ -5,7 +5,16 @@ import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel ...@@ -5,7 +5,16 @@ import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import com.stfalcon.frescoimageviewer.ImageViewer import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.* import kotlinx.android.synthetic.main.message_attachment.view.*
class ImageAttachmentViewHolder(itemView: View) : BaseViewHolder<ImageAttachmentViewModel>(itemView) { class ImageAttachmentViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener) {
init {
with(itemView) {
setupActionMenu(attachment_container)
setupActionMenu(image_attachment)
}
}
override fun bindViews(data: ImageAttachmentViewModel) { override fun bindViews(data: ImageAttachmentViewModel) {
with(itemView) { with(itemView) {
image_attachment.setImageURI(data.attachmentUrl) image_attachment.setImageURI(data.attachmentUrl)
......
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.MenuItem
import android.view.View import android.view.View
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import kotlinx.android.synthetic.main.avatar.view.* import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.* import kotlinx.android.synthetic.main.item_message.view.*
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
class MessageViewHolder( class MessageViewHolder(
itemView: View, itemView: View,
private val roomType: String, listener: ActionsListener
private val roomName: String, ) : BaseViewHolder<MessageViewModel>(itemView, listener) {
private val presenter: ChatRoomPresenter?,
enableActions: Boolean
) : BaseViewHolder<MessageViewModel>(itemView),
MenuItem.OnMenuItemClickListener {
init { init {
itemView.text_content.movementMethod = LinkMovementMethod() with(itemView) {
text_content.movementMethod = LinkMovementMethod()
if (enableActions) { setupActionMenu(text_content)
itemView.setOnLongClickListener {
if (data?.isSystemMessage == false) {
val menuItems = it.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = data?.isPinned ?: false
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = isPinned
}
val adapter = ActionListAdapter(menuItems, this@MessageViewHolder)
BottomSheetMenu(adapter).apply {
}.show(it.context)
}
true
}
} }
} }
...@@ -52,27 +26,4 @@ class MessageViewHolder( ...@@ -52,27 +26,4 @@ class MessageViewHolder(
image_avatar.setImageURI(data.avatar) image_avatar.setImageURI(data.avatar)
} }
} }
override fun onMenuItemClick(item: MenuItem): Boolean {
data?.rawData?.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter?.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter?.citeMessage(roomType, roomName, id, false)
R.id.action_menu_msg_reply -> presenter?.citeMessage(roomType, roomName, id, true)
R.id.action_menu_msg_copy -> presenter?.copyMessage(id)
R.id.action_menu_msg_edit -> presenter?.editMessage(roomId, id, message)
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
presenter?.pinMessage(id)
} else {
presenter?.unpinMessage(id)
}
}
}
else -> TODO("Not implemented")
}
}
return true
}
} }
\ No newline at end of file
...@@ -8,7 +8,15 @@ import chat.rocket.android.util.extensions.content ...@@ -8,7 +8,15 @@ import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import kotlinx.android.synthetic.main.message_url_preview.view.* import kotlinx.android.synthetic.main.message_url_preview.view.*
class UrlPreviewViewHolder(itemView: View) : BaseViewHolder<UrlPreviewViewModel>(itemView) { class UrlPreviewViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<UrlPreviewViewModel>(itemView, listener) {
init {
with(itemView) {
setupActionMenu(url_preview_layout)
}
}
override fun bindViews(data: UrlPreviewViewModel) { override fun bindViews(data: UrlPreviewViewModel) {
with(itemView) { with(itemView) {
if (data.thumbUrl.isNullOrEmpty()) { if (data.thumbUrl.isNullOrEmpty()) {
......
...@@ -6,11 +6,15 @@ import chat.rocket.android.player.PlayerActivity ...@@ -6,11 +6,15 @@ import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import kotlinx.android.synthetic.main.message_attachment.view.* import kotlinx.android.synthetic.main.message_attachment.view.*
class VideoAttachmentViewHolder(itemView: View) : BaseViewHolder<VideoAttachmentViewModel>(itemView) { class VideoAttachmentViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<VideoAttachmentViewModel>(itemView, listener) {
init { init {
with(itemView) { with(itemView) {
image_attachment.setVisible(false) image_attachment.setVisible(false)
audio_video_attachment.setVisible(true) audio_video_attachment.setVisible(true)
setupActionMenu(attachment_container)
setupActionMenu(audio_video_attachment)
} }
} }
......
...@@ -6,23 +6,23 @@ import chat.rocket.android.chatroom.domain.UriInteractor ...@@ -6,23 +6,23 @@ import chat.rocket.android.chatroom.domain.UriInteractor
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.connect
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.rest.* import chat.rocket.core.internal.rest.*
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.Value import chat.rocket.core.model.Value
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import org.threeten.bp.Instant
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -34,14 +34,22 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -34,14 +34,22 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val permissions: GetPermissionsInteractor, private val permissions: GetPermissionsInteractor,
private val uriInteractor: UriInteractor, private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository, private val messagesRepository: MessagesRepository,
factory: RocketChatClientFactory, factory: ConnectionManagerFactory,
private val mapper: ViewModelMapper) { private val mapper: ViewModelMapper) {
private val client = factory.create(serverInteractor.get()!!) private val currentServer = serverInteractor.get()!!
private var subId: String? = null private val manager = factory.create(currentServer)
private val client = manager.client
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private val messagesChannel = Channel<Message>()
private var chatRoomId: String? = null
private var chatRoomType: String? = null
private val stateChannel = Channel<State>() private val stateChannel = Channel<State>()
private var lastState = manager.state
fun loadMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) { fun loadMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
this.chatRoomId = chatRoomId
this.chatRoomType = chatRoomType
launchUI(strategy) { launchUI(strategy) {
view.showLoading() view.showLoading()
try { try {
...@@ -56,10 +64,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -56,10 +64,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
val messagesViewModels = mapper.map(messages) val messagesViewModels = mapper.map(messages)
view.showMessages(messagesViewModels) view.showMessages(messagesViewModels)
// Subscribe after getting the first page of messages from REST subscribeMessages(chatRoomId)
if (offset == 0L) {
subscribeMessages(chatRoomId)
}
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace() ex.printStackTrace()
ex.message?.let { ex.message?.let {
...@@ -70,6 +75,10 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -70,6 +75,10 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} finally { } finally {
view.hideLoading() view.hideLoading()
} }
if (offset == 0L) {
subscribeState()
}
} }
} }
...@@ -132,7 +141,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -132,7 +141,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} }
} }
fun markRoomAsRead(roomId: String) { private fun markRoomAsRead(roomId: String) {
launchUI(strategy) { launchUI(strategy) {
try { try {
client.markAsRead(roomId) client.markAsRead(roomId)
...@@ -143,57 +152,70 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -143,57 +152,70 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} }
} }
private fun subscribeMessages(roomId: String) { private fun subscribeState() {
client.addStateChannel(stateChannel) Timber.d("Subscribing to Status changes")
lastState = manager.state
manager.addStatusChannel(stateChannel)
launch(CommonPool + strategy.jobs) { launch(CommonPool + strategy.jobs) {
for (status in stateChannel) { for (state in stateChannel) {
Timber.d("Changing status to: $status") Timber.d("Got new state: $state - last: $lastState")
when (status) { if (state != lastState) {
State.Authenticating -> Timber.d("Authenticating") launch(UI) {
State.Connected -> { view.showConnectionState(state)
Timber.d("Connected")
subId = client.subscribeRoomMessages(roomId) {
Timber.d("subscribe messages for $roomId: $it")
}
} }
}
}
Timber.d("Done on statusChannel")
}
when (client.state) { if (state is State.Connected) {
State.Connected -> { loadMissingMessages()
Timber.d("Already connected") }
subId = client.subscribeRoomMessages(roomId) {
Timber.d("subscribe messages for $roomId: $it")
} }
lastState = state
} }
else -> client.connect()
} }
}
launchUI(strategy) { private fun subscribeMessages(roomId: String) {
listenMessages(roomId) manager.subscribeRoomMessages(roomId, messagesChannel)
}
// TODO - when we have a proper service, we won't need to take care of connection, just launch(CommonPool + strategy.jobs) {
// subscribe and listen... for (message in messagesChannel) {
/*launchUI(strategy) { Timber.d("New message for room ${message.roomId}")
subId = client.subscribeRoomMessages(roomId) { updateMessage(message)
Timber.d("subscribe messages for $roomId: $it")
} }
listenMessages(roomId) }
}*/
} }
fun unsubscribeMessages() { private fun loadMissingMessages() {
launch(CommonPool) { launch(parent = strategy.jobs) {
client.removeStateChannel(stateChannel) if (chatRoomId != null && chatRoomType != null) {
subId?.let { subscriptionId -> val roomType = roomTypeOf(chatRoomType!!)
client.unsubscribe(subscriptionId) val lastMessage = messagesRepository.getByRoomId(chatRoomId!!).sortedByDescending { it.timestamp }.first()
val instant = Instant.ofEpochMilli(lastMessage.timestamp)
val messages = client.history(chatRoomId!!, roomType, count = 50,
oldest = instant.toString())
Timber.d("History: $messages")
if (messages.result.isNotEmpty()) {
val models = mapper.map(messages.result)
messagesRepository.saveAll(messages.result)
launchUI(strategy) {
view.showNewMessage(models)
}
if (messages.result.size == 50) {
// we loade at least count messages, try one more to fetch more messages
loadMissingMessages()
}
}
} }
} }
} }
fun unsubscribeMessages(chatRoomId: String) {
manager.removeStatusChannel(stateChannel)
manager.unsubscribeRoomMessages(chatRoomId)
}
/** /**
* Delete the message with the given id. * Delete the message with the given id.
* *
...@@ -320,17 +342,6 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -320,17 +342,6 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
fun toMembersList(chatRoomId: String, chatRoomType: String) = navigator.toMembersList(chatRoomId, chatRoomType) fun toMembersList(chatRoomId: String, chatRoomType: String) = navigator.toMembersList(chatRoomId, chatRoomType)
private suspend fun listenMessages(roomId: String) {
launch(CommonPool + strategy.jobs) {
for (message in client.messagesChannel) {
if (message.roomId != roomId) {
Timber.d("Ignoring message for room ${message.roomId}, expecting $roomId")
}
updateMessage(message)
}
}
}
private fun updateMessage(streamedMessage: Message) { private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) { launchUI(strategy) {
val viewModelStreamedMessage = mapper.map(streamedMessage) val viewModelStreamedMessage = mapper.map(streamedMessage)
......
...@@ -4,6 +4,7 @@ import android.net.Uri ...@@ -4,6 +4,7 @@ import android.net.Uri
import chat.rocket.android.chatroom.viewmodel.BaseViewModel import chat.rocket.android.chatroom.viewmodel.BaseViewModel
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.State
interface ChatRoomView : LoadingView, MessageView { interface ChatRoomView : LoadingView, MessageView {
...@@ -97,4 +98,6 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -97,4 +98,6 @@ interface ChatRoomView : LoadingView, MessageView {
fun clearMessageComposition() fun clearMessageComposition()
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int) fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
fun showConnectionState(state: State)
} }
\ No newline at end of file
package chat.rocket.android.chatroom.presentation package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetChatRoomsInteractor import chat.rocket.android.server.domain.GetChatRoomsInteractor
...@@ -12,6 +11,7 @@ import chat.rocket.common.RocketChatException ...@@ -12,6 +11,7 @@ import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getRoomPinnedMessages import chat.rocket.core.internal.rest.getRoomPinnedMessages
import chat.rocket.core.model.Value import chat.rocket.core.model.Value
import chat.rocket.core.model.isSystemMessage
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -42,8 +42,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag ...@@ -42,8 +42,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
val pinnedMessages = val pinnedMessages =
client.getRoomPinnedMessages(roomId, room.type, pinnedMessagesListOffset) client.getRoomPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
pinnedMessagesListOffset = pinnedMessages.offset.toInt() pinnedMessagesListOffset = pinnedMessages.offset.toInt()
val messageList = mapper.map(pinnedMessages.result) val messageList = mapper.map(pinnedMessages.result.filterNot { it.isSystemMessage() })
.filter { it is MessageViewModel}.filterNot { (it as MessageViewModel).isSystemMessage }
view.showPinnedMessages(messageList) view.showPinnedMessages(messageList)
view.hideLoading() view.hideLoading()
}.ifNull { }.ifNull {
......
...@@ -6,6 +6,8 @@ import android.os.Bundle ...@@ -6,6 +6,8 @@ import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.addFragment import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
...@@ -32,6 +34,11 @@ private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only" ...@@ -32,6 +34,11 @@ private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
// TODO - workaround for now... We will move to a single activity
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var managerFactory: ConnectionManagerFactory
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
...@@ -42,6 +49,9 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -42,6 +49,9 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat_room) setContentView(R.layout.activity_chat_room)
// Workaround for when we are coming to the app via the recents app and the app was killed.
managerFactory.create(serverInteractor.get()!!).connect()
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID) chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" } requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
......
...@@ -27,6 +27,7 @@ import chat.rocket.android.widget.emoji.ComposerEditText ...@@ -27,6 +27,7 @@ import chat.rocket.android.widget.emoji.ComposerEditText
import chat.rocket.android.widget.emoji.Emoji import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiKeyboardPopup import chat.rocket.android.widget.emoji.EmojiKeyboardPopup
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.internal.realtime.State
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_chat_room.* import kotlinx.android.synthetic.main.fragment_chat_room.*
...@@ -114,7 +115,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -114,7 +115,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
} }
override fun onDestroyView() { override fun onDestroyView() {
presenter.unsubscribeMessages() presenter.unsubscribeMessages(chatRoomId)
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
unsubscribeTextMessage() unsubscribeTextMessage()
super.onDestroyView() super.onDestroyView()
...@@ -225,6 +226,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -225,6 +226,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
actionSnackbar.title = username actionSnackbar.title = username
actionSnackbar.text = quotedMessage actionSnackbar.text = quotedMessage
actionSnackbar.show() actionSnackbar.show()
KeyboardHelper.showSoftKeyboard(text_message)
if (!recycler_view.isAtBottom()) {
if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0)
}
}
} }
} }
...@@ -252,6 +259,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -252,6 +259,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
actionSnackbar.show() actionSnackbar.show()
text_message.textContent = text text_message.textContent = text
editingMessageId = messageId editingMessageId = messageId
KeyboardHelper.showSoftKeyboard(text_message)
} }
} }
...@@ -288,6 +296,28 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -288,6 +296,28 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
showMessage(getString(R.string.max_file_size_exceeded, fileSize, maxFileSize)) showMessage(getString(R.string.max_file_size_exceeded, fileSize, maxFileSize))
} }
override fun showConnectionState(state: State) {
activity?.apply {
connection_status_text.fadeIn()
handler.removeCallbacks(dismissStatus)
when (state) {
is State.Connected -> {
connection_status_text.text = getString(R.string.status_connected)
handler.postDelayed(dismissStatus, 2000)
}
is State.Disconnected -> connection_status_text.text = getString(R.string.status_disconnected)
is State.Connecting -> connection_status_text.text = getString(R.string.status_connecting)
is State.Authenticating -> connection_status_text.text = getString(R.string.status_authenticating)
is State.Disconnecting -> connection_status_text.text = getString(R.string.status_disconnecting)
is State.Waiting -> connection_status_text.text = getString(R.string.status_waiting, state.seconds)
}
}
}
private val dismissStatus = {
connection_status_text.fadeOut()
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() { recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
...@@ -320,6 +350,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -320,6 +350,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
emojiKeyboardPopup.listener = this emojiKeyboardPopup.listener = this
text_message.listener = object : ComposerEditText.ComposerEditTextListener { text_message.listener = object : ComposerEditText.ComposerEditTextListener {
override fun onKeyboardOpened() { override fun onKeyboardOpened() {
if (recycler_view.isAtBottom()) {
if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0)
}
}
} }
override fun onKeyboardClosed() { override fun onKeyboardClosed() {
...@@ -395,6 +430,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -395,6 +430,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
actionSnackbar = ActionSnackbar.make(message_list_container, parser = parser) actionSnackbar = ActionSnackbar.make(message_list_container, parser = parser)
actionSnackbar.cancelView.setOnClickListener({ actionSnackbar.cancelView.setOnClickListener({
clearMessageComposition() clearMessageComposition()
KeyboardHelper.showSoftKeyboard(text_message)
}) })
} }
......
package chat.rocket.android.chatroom.viewmodel package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.AudioAttachment import chat.rocket.core.model.attachment.AudioAttachment
data class AudioAttachmentViewModel( data class AudioAttachmentViewModel(
override val message: Message,
override val rawData: AudioAttachment, override val rawData: AudioAttachment,
override val messageId: String, override val messageId: String,
override val attachmentUrl: String, override val attachmentUrl: String,
......
package chat.rocket.android.chatroom.viewmodel package chat.rocket.android.chatroom.viewmodel
import chat.rocket.core.model.Message
import java.security.InvalidParameterException import java.security.InvalidParameterException
interface BaseViewModel<out T> { interface BaseViewModel<out T> {
val message: Message
val rawData: T val rawData: T
val messageId: String val messageId: String
val viewType: Int val viewType: Int
......
package chat.rocket.android.chatroom.viewmodel package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.ImageAttachment import chat.rocket.core.model.attachment.ImageAttachment
data class ImageAttachmentViewModel( data class ImageAttachmentViewModel(
override val message: Message,
override val rawData: ImageAttachment, override val rawData: ImageAttachment,
override val messageId: String, override val messageId: String,
override val attachmentUrl: String, override val attachmentUrl: String,
......
...@@ -4,14 +4,14 @@ import chat.rocket.android.R ...@@ -4,14 +4,14 @@ import chat.rocket.android.R
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
data class MessageViewModel( data class MessageViewModel(
override val message: Message,
override val rawData: Message, override val rawData: Message,
override val messageId: String, override val messageId: String,
override val avatar: String, override val avatar: String,
override val time: CharSequence, override val time: CharSequence,
override val senderName: CharSequence, override val senderName: CharSequence,
override val content: CharSequence, override val content: CharSequence,
override val isPinned: Boolean, override val isPinned: Boolean
val isSystemMessage: Boolean
) : BaseMessageViewModel<Message> { ) : BaseMessageViewModel<Message> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType get() = BaseViewModel.ViewType.MESSAGE.viewType
......
package chat.rocket.android.chatroom.viewmodel package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.url.Url import chat.rocket.core.model.url.Url
data class UrlPreviewViewModel( data class UrlPreviewViewModel(
override val message: Message,
override val rawData: Url, override val rawData: Url,
override val messageId: String, override val messageId: String,
val title: CharSequence?, val title: CharSequence?,
......
package chat.rocket.android.chatroom.viewmodel package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.VideoAttachment import chat.rocket.core.model.attachment.VideoAttachment
data class VideoAttachmentViewModel( data class VideoAttachmentViewModel(
override val message: Message,
override val rawData: VideoAttachment, override val rawData: VideoAttachment,
override val messageId: String, override val messageId: String,
override val attachmentUrl: String, override val attachmentUrl: String,
......
...@@ -19,11 +19,13 @@ import chat.rocket.core.model.Message ...@@ -19,11 +19,13 @@ import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType import chat.rocket.core.model.MessageType
import chat.rocket.core.model.Value import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.* import chat.rocket.core.model.attachment.*
import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
import okhttp3.HttpUrl import okhttp3.HttpUrl
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException
import javax.inject.Inject import javax.inject.Inject
class ViewModelMapper @Inject constructor(private val context: Context, class ViewModelMapper @Inject constructor(private val context: Context,
...@@ -53,7 +55,7 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -53,7 +55,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
return@withContext list return@withContext list
} }
private suspend fun translate(message: Message): List<BaseViewModel<*>> = withContext(CommonPool) { private suspend fun translate(message: Message): List<BaseViewModel<*>> = withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>() val list = ArrayList<BaseViewModel<*>>()
message.urls?.forEach { message.urls?.forEach {
...@@ -81,7 +83,7 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -81,7 +83,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val title = url.meta?.title val title = url.meta?.title
val description = url.meta?.description val description = url.meta?.description
return UrlPreviewViewModel(url, message.id, title, hostname, description, thumb) return UrlPreviewViewModel(message, url, message.id, title, hostname, description, thumb)
} }
private fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? { private fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? {
...@@ -96,12 +98,12 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -96,12 +98,12 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val attachmentTitle = attachment.title val attachmentTitle = attachment.title
val id = "${message.id}_${attachment.titleLink}".hashCode().toLong() val id = "${message.id}_${attachment.titleLink}".hashCode().toLong()
return when (attachment) { return when (attachment) {
is ImageAttachment -> ImageAttachmentViewModel(attachment, message.id, attachmentUrl, is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id,
attachmentTitle ?: "", id) attachmentUrl, attachmentTitle ?: "", id)
is VideoAttachment -> VideoAttachmentViewModel(attachment, message.id, is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id)
is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id) attachmentUrl, attachmentTitle ?: "", id)
is AudioAttachment -> AudioAttachmentViewModel(attachment,
message.id, attachmentUrl, attachmentTitle ?: "", id)
else -> null else -> null
} }
} }
...@@ -143,13 +145,16 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -143,13 +145,16 @@ class ViewModelMapper @Inject constructor(private val context: Context,
} }
val content = getContent(context, message, quote) val content = getContent(context, message, quote)
MessageViewModel(rawData = message, messageId = message.id, MessageViewModel(message = message, rawData = message, messageId = message.id,
avatar = avatar!!, time = time, senderName = sender, avatar = avatar!!, time = time, senderName = sender,
content = content.first, isPinned = message.pinned, content = content, isPinned = message.pinned)
isSystemMessage = content.second)
} }
private fun getSenderName(message: Message): CharSequence { private fun getSenderName(message: Message): CharSequence {
if (!message.senderAlias.isNullOrEmpty()) {
return message.senderAlias!!
}
val username = message.sender?.username val username = message.sender?.username
val realName = message.sender?.name val realName = message.sender?.name
val senderName = if (settings.useRealName()) realName else username val senderName = if (settings.useRealName()) realName else username
...@@ -157,6 +162,10 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -157,6 +162,10 @@ class ViewModelMapper @Inject constructor(private val context: Context,
} }
private fun getUserAvatar(message: Message): String? { private fun getUserAvatar(message: Message): String? {
message.avatar?.let {
return it // Always give preference for overridden avatar from message
}
val username = message.sender?.username ?: "?" val username = message.sender?.username ?: "?"
return baseUrl?.let { return baseUrl?.let {
UrlHelper.getAvatarUrl(baseUrl, username) UrlHelper.getAvatarUrl(baseUrl, username)
...@@ -171,28 +180,14 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -171,28 +180,14 @@ class ViewModelMapper @Inject constructor(private val context: Context,
Timber.d("Will quote message Id: $msgIdToQuote") Timber.d("Will quote message Id: $msgIdToQuote")
return if (msgIdToQuote != null) messagesRepository.getById(msgIdToQuote) else null return if (msgIdToQuote != null) messagesRepository.getById(msgIdToQuote) else null
} }
return null return null
} }
private suspend fun getContent(context: Context, message: Message, quote: Message?): Pair<CharSequence, Boolean> { private suspend fun getContent(context: Context, message: Message, quote: Message?): CharSequence {
var systemMessage = true return when (message.isSystemMessage()) {
val content = when (message.type) { true -> getSystemMessage(message, context)
//TODO: Add implementation for Welcome type. false -> getNormalMessage(message, quote)
is MessageType.MessageRemoved -> getSystemMessage(context.getString(R.string.message_removed))
is MessageType.UserJoined -> getSystemMessage(context.getString(R.string.message_user_joined_channel))
is MessageType.UserLeft -> getSystemMessage(context.getString(R.string.message_user_left))
is MessageType.UserAdded -> getSystemMessage(context.getString(R.string.message_user_added_by, message.message, message.sender?.username))
is MessageType.RoomNameChanged -> getSystemMessage(context.getString(R.string.message_room_name_changed, message.message, message.sender?.username))
is MessageType.UserRemoved -> getSystemMessage(context.getString(R.string.message_user_removed_by, message.message, message.sender?.username))
is MessageType.MessagePinned -> getSystemMessage(context.getString(R.string.message_pinned))
else -> {
systemMessage = false
getNormalMessage(message, quote)
}
} }
return Pair(content, systemMessage)
} }
private suspend fun getNormalMessage(message: Message, quote: Message?): CharSequence { private suspend fun getNormalMessage(message: Message, quote: Message?): CharSequence {
...@@ -204,7 +199,32 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -204,7 +199,32 @@ class ViewModelMapper @Inject constructor(private val context: Context,
return parser.renderMarkdown(message.message, quoteViewModel, currentUsername) return parser.renderMarkdown(message.message, quoteViewModel, currentUsername)
} }
private fun getSystemMessage(content: String): CharSequence { private fun getSystemMessage(message: Message, context: Context): CharSequence {
val content = when (message.type) {
//TODO: Add implementation for Welcome type.
is MessageType.MessageRemoved -> context.getString(R.string.message_removed)
is MessageType.UserJoined -> context.getString(R.string.message_user_joined_channel)
is MessageType.UserLeft -> context.getString(R.string.message_user_left)
is MessageType.UserAdded -> context.getString(R.string.message_user_added_by, message.message, message.sender?.username)
is MessageType.RoomNameChanged -> context.getString(R.string.message_room_name_changed, message.message, message.sender?.username)
is MessageType.UserRemoved -> context.getString(R.string.message_user_removed_by, message.message, message.sender?.username)
is MessageType.MessagePinned -> {
val attachment = message.attachments?.get(0)
val pinnedSystemMessage = context.getString(R.string.message_pinned)
if (attachment != null && attachment is MessageAttachment) {
return SpannableStringBuilder(pinnedSystemMessage)
.apply {
setSpan(StyleSpan(Typeface.ITALIC), 0, length, 0)
setSpan(ForegroundColorSpan(Color.GRAY), 0, length, 0)
}
.append(quoteMessage(attachment.author!!, attachment.text!!, attachment.timestamp!!))
}
return pinnedSystemMessage
}
else -> {
throw InvalidParameterException("Invalid message type: ${message.type}")
}
}
//isSystemMessage = true //isSystemMessage = true
val spannableMsg = SpannableStringBuilder(content) val spannableMsg = SpannableStringBuilder(content)
spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length, spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length,
......
package chat.rocket.android.chatrooms.presentation package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetChatRoomsInteractor import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.server.domain.RefreshSettingsInteractor import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.domain.SaveChatRoomsInteractor import chat.rocket.android.server.infraestructure.chatRooms
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.RoomType
import chat.rocket.core.internal.model.Subscription import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.* import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.rest.chatRooms import chat.rocket.core.internal.realtime.StreamMessage
import chat.rocket.core.internal.realtime.Type
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Room import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.* import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.Channel
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -26,31 +29,47 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -26,31 +29,47 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val getChatRoomsInteractor: GetChatRoomsInteractor, private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor, private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor, private val refreshSettingsInteractor: RefreshSettingsInteractor,
factory: RocketChatClientFactory) { settingsRepository: SettingsRepository,
private val client: RocketChatClient = factory.create(serverInteractor.get()!!) factory: ConnectionManagerFactory) {
private val manager: ConnectionManager = factory.create(serverInteractor.get()!!)
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private var reloadJob: Deferred<List<ChatRoom>>? = null private var reloadJob: Deferred<List<ChatRoom>>? = null
private val settings = settingsRepository.get(currentServer)!!
private val subscriptionsChannel = Channel<StreamMessage<BaseRoom>>()
private val stateChannel = Channel<State>() private val stateChannel = Channel<State>()
private var lastState = manager.state
fun loadChatRooms() { fun loadChatRooms() {
refreshSettingsInteractor.refreshAsync(currentServer) refreshSettingsInteractor.refreshAsync(currentServer)
launchUI(strategy) { launchUI(strategy) {
view.showLoading() view.showLoading()
subscribeStatusChange()
try { try {
view.updateChatRooms(loadRooms()) view.updateChatRooms(loadRooms())
subscribeRoomUpdates()
} catch (e: RocketChatException) { } catch (e: RocketChatException) {
Timber.e(e) Timber.e(e)
view.showMessage(e.message!!) view.showMessage(e.message!!)
} finally { } finally {
view.hideLoading() view.hideLoading()
} }
subscribeRoomUpdates()
} }
} }
fun loadChatRoom(chatRoom: ChatRoom) = navigator.toChatRoom(chatRoom.id, chatRoom.name, fun loadChatRoom(chatRoom: ChatRoom) {
chatRoom.type.toString(), chatRoom.readonly ?: false) val roomName = if (chatRoom.type is RoomType.DirectMessage
&& chatRoom.fullName != null
&& settings.useRealName()) {
chatRoom.fullName!!
} else {
chatRoom.name
}
navigator.toChatRoom(chatRoom.id, roomName,
chatRoom.type.toString(), chatRoom.readonly ?: false)
}
/** /**
* Gets a [ChatRoom] list from local repository. * Gets a [ChatRoom] list from local repository.
...@@ -65,8 +84,9 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -65,8 +84,9 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
} }
private suspend fun loadRooms(): List<ChatRoom> { private suspend fun loadRooms(): List<ChatRoom> {
val chatRooms = client.chatRooms().update val chatRooms = manager.chatRooms().update
val sortedRooms = sortRooms(chatRooms) val sortedRooms = sortRooms(chatRooms)
Timber.d("Loaded rooms: ${sortedRooms.size}")
saveChatRoomsInteractor.save(currentServer, sortedRooms) saveChatRoomsInteractor.save(currentServer, sortedRooms)
return sortedRooms return sortedRooms
} }
...@@ -77,6 +97,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -77,6 +97,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
} }
private fun updateRooms() { private fun updateRooms() {
Timber.d("Updating Rooms")
launch { launch {
view.updateChatRooms(getChatRoomsInteractor.get(currentServer)) view.updateChatRooms(getChatRoomsInteractor.get(currentServer))
} }
...@@ -92,104 +113,98 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -92,104 +113,98 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
} }
} }
// TODO - Temporary stuff, remove when adding DB support private suspend fun subscribeStatusChange() {
private suspend fun subscribeRoomUpdates() { lastState = manager.state
client.addStateChannel(stateChannel)
launch(CommonPool + strategy.jobs) { launch(CommonPool + strategy.jobs) {
for (status in stateChannel) { for (state in stateChannel) {
Timber.d("Changing status to: $status") Timber.d("Got new state: $state - last: $lastState")
when (status) { if (state != lastState) {
State.Authenticating -> Timber.d("Authenticating") launch(UI) {
State.Connected -> { view.showConnectionState(state)
Timber.d("Connected")
client.subscribeSubscriptions {
Timber.d("subscriptions: $it")
}
client.subscribeRooms {
Timber.d("rooms: $it")
}
} }
}
}
Timber.d("Done on statusChannel")
}
when (client.state) { if (state is State.Connected) {
State.Connected -> { reloadRooms()
Timber.d("Already connected") updateRooms()
} }
else -> client.connect() }
} lastState = state
launch(CommonPool + strategy.jobs) {
for (message in client.roomsChannel) {
Timber.d("Got message: $message")
updateRoom(message)
} }
} }
}
// TODO - Temporary stuff, remove when adding DB support
private suspend fun subscribeRoomUpdates() {
manager.addStatusChannel(stateChannel)
manager.addRoomsAndSubscriptionsChannel(subscriptionsChannel)
launch(CommonPool + strategy.jobs) { launch(CommonPool + strategy.jobs) {
for (message in client.subscriptionsChannel) { for (message in subscriptionsChannel) {
Timber.d("Got message: $message") Timber.d("Got message: $message")
updateSubscription(message) when (message.data) {
is Room -> updateRoom(message as StreamMessage<Room>)
is Subscription -> updateSubscription(message as StreamMessage<Subscription>)
}
} }
} }
} }
private fun updateRoom(message: StreamMessage<Room>) { private suspend fun updateRoom(message: StreamMessage<Room>) {
launchUI(strategy) { Timber.d("Update Room: ${message.type} - ${message.data.id} - ${message.data.name}")
when (message.type) { when (message.type) {
Type.Removed -> { Type.Removed -> {
removeRoom(message.data.id) removeRoom(message.data.id)
} }
Type.Updated -> { Type.Updated -> {
updateRoom(message.data) updateRoom(message.data)
} }
Type.Inserted -> { Type.Inserted -> {
// On insertion, just get all chatrooms again, since we can't create one just // On insertion, just get all chatrooms again, since we can't create one just
// from a Room // from a Room
reloadRooms() reloadRooms()
}
} }
updateRooms()
} }
updateRooms()
} }
private fun updateSubscription(message: StreamMessage<Subscription>) { private suspend fun updateSubscription(message: StreamMessage<Subscription>) {
launchUI(strategy) { Timber.d("Update Subscription: ${message.type} - ${message.data.id} - ${message.data.name}")
when (message.type) { when (message.type) {
Type.Removed -> { Type.Removed -> {
removeRoom(message.data.roomId) removeRoom(message.data.roomId)
} }
Type.Updated -> { Type.Updated -> {
updateSubscription(message.data) updateSubscription(message.data)
} }
Type.Inserted -> { Type.Inserted -> {
// On insertion, just get all chatrooms again, since we can't create one just // On insertion, just get all chatrooms again, since we can't create one just
// from a Subscription // from a Subscription
reloadRooms() reloadRooms()
}
} }
updateRooms()
} }
updateRooms()
} }
private suspend fun reloadRooms() { private suspend fun reloadRooms() {
Timber.d("realoadRooms()") Timber.d("realoadRooms()")
reloadJob?.cancel() reloadJob?.cancel()
reloadJob = async(CommonPool + strategy.jobs) { try {
delay(1000) reloadJob = async(CommonPool + strategy.jobs) {
Timber.d("reloading rooms after wait") delay(1000)
loadRooms() Timber.d("reloading rooms after wait")
loadRooms()
}
reloadJob?.await()
} catch (ex: Exception) {
ex.printStackTrace()
} }
reloadJob?.await()
} }
// Update a ChatRoom with a Room information // Update a ChatRoom with a Room information
private fun updateRoom(room: Room) { private fun updateRoom(room: Room) {
Timber.d("Updating Room: ${room.id} - ${room.name}")
val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList() val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == room.id } val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == room.id }
chatRoom?.apply { chatRoom?.apply {
...@@ -220,6 +235,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -220,6 +235,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
// Update a ChatRoom with a Subscription information // Update a ChatRoom with a Subscription information
private fun updateSubscription(subscription: Subscription) { private fun updateSubscription(subscription: Subscription) {
Timber.d("Updating subscrition: ${subscription.id} - ${subscription.name}")
val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList() val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId } val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId }
chatRoom?.apply { chatRoom?.apply {
...@@ -251,6 +267,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -251,6 +267,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private fun removeRoom(id: String, private fun removeRoom(id: String,
chatRooms: MutableList<ChatRoom> = getChatRoomsInteractor.get(currentServer).toMutableList()) { chatRooms: MutableList<ChatRoom> = getChatRoomsInteractor.get(currentServer).toMutableList()) {
Timber.d("Removing ROOM: $id")
synchronized(this) { synchronized(this) {
chatRooms.removeAll { chatRoom -> chatRoom.id == id } chatRooms.removeAll { chatRoom -> chatRoom.id == id }
} }
...@@ -258,7 +275,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -258,7 +275,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
} }
fun disconnect() { fun disconnect() {
client.removeStateChannel(stateChannel) manager.removeStatusChannel(stateChannel)
client.disconnect() manager.removeRoomsAndSubscriptionsChannel(subscriptionsChannel)
} }
} }
\ No newline at end of file
...@@ -2,6 +2,7 @@ package chat.rocket.android.chatrooms.presentation ...@@ -2,6 +2,7 @@ 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.State
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
interface ChatRoomsView : LoadingView, MessageView { interface ChatRoomsView : LoadingView, MessageView {
...@@ -17,4 +18,6 @@ interface ChatRoomsView : LoadingView, MessageView { ...@@ -17,4 +18,6 @@ interface ChatRoomsView : LoadingView, MessageView {
* Shows no chat rooms to display. * Shows no chat rooms to display.
*/ */
fun showNoChatRoomsToDisplay() fun showNoChatRoomsToDisplay()
fun showConnectionState(state: State)
} }
\ No newline at end of file
...@@ -9,6 +9,8 @@ import android.view.ViewGroup ...@@ -9,6 +9,8 @@ import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.content import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
...@@ -21,6 +23,7 @@ import kotlinx.android.synthetic.main.item_chat.view.* ...@@ -21,6 +23,7 @@ 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 ChatRoomsAdapter(private val context: Context, class ChatRoomsAdapter(private val context: Context,
private val settings: PublicSettings,
private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() { private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() {
var dataSet: MutableList<ChatRoom> = ArrayList() var dataSet: MutableList<ChatRoom> = ArrayList()
...@@ -69,7 +72,11 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -69,7 +72,11 @@ class ChatRoomsAdapter(private val context: Context,
} }
private fun bindName(chatRoom: ChatRoom, textView: TextView) { private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.content = chatRoom.name if (chatRoom.type is RoomType.DirectMessage && settings.useRealName()) {
textView.content = chatRoom.fullName ?: chatRoom.name
} else {
textView.content = chatRoom.name
}
} }
private fun bindLastMessageDateTime(chatRoom: ChatRoom, textView: TextView) { private fun bindLastMessageDateTime(chatRoom: ChatRoom, textView: TextView) {
......
package chat.rocket.android.chatrooms.ui package chat.rocket.android.chatrooms.ui
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.util.DiffUtil import android.support.v7.util.DiffUtil
...@@ -11,14 +12,16 @@ import android.view.* ...@@ -11,14 +12,16 @@ import android.view.*
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.DividerItemDecoration import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.model.ChatRoom 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.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
...@@ -26,7 +29,12 @@ import javax.inject.Inject ...@@ -26,7 +29,12 @@ import javax.inject.Inject
class ChatRoomsFragment : Fragment(), ChatRoomsView { class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject lateinit var presenter: ChatRoomsPresenter @Inject lateinit var presenter: ChatRoomsPresenter
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var settingsRepository: SettingsRepository
private var searchView: SearchView? = null private var searchView: SearchView? = null
private val handler = Handler()
private var listJob: Job? = null
companion object { companion object {
fun newInstance() = ChatRoomsFragment() fun newInstance() = ChatRoomsFragment()
...@@ -39,6 +47,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -39,6 +47,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
override fun onDestroy() { override fun onDestroy() {
handler.removeCallbacks(dismissStatus)
presenter.disconnect() presenter.disconnect()
super.onDestroy() super.onDestroy()
} }
...@@ -72,14 +81,19 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -72,14 +81,19 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) { override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) {
activity.apply { activity.apply {
launch(UI) { listJob?.cancel()
listJob = launch(UI) {
val adapter = recycler_view.adapter as ChatRoomsAdapter val adapter = recycler_view.adapter as ChatRoomsAdapter
// FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android.dev/issues/5a90d4718cb3c2fa63b3f557?time=last-seven-days
// TODO - fix this bug to reenable DiffUtil
val diff = async(CommonPool) { val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.dataSet, newDataSet)) DiffUtil.calculateDiff(RoomsDiffCallback(adapter.dataSet, newDataSet))
}.await() }.await()
adapter.updateRooms(newDataSet) if (isActive) {
diff.dispatchUpdatesTo(adapter) adapter.updateRooms(newDataSet)
diff.dispatchUpdatesTo(adapter)
}
} }
} }
} }
...@@ -96,6 +110,28 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -96,6 +110,28 @@ 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) {
activity?.apply {
connection_status_text.fadeIn()
handler.removeCallbacks(dismissStatus)
when (state) {
is State.Connected -> {
connection_status_text.text = getString(R.string.status_connected)
handler.postDelayed(dismissStatus, 2000)
}
is State.Disconnected -> connection_status_text.text = getString(R.string.status_disconnected)
is State.Connecting -> connection_status_text.text = getString(R.string.status_connecting)
is State.Authenticating -> connection_status_text.text = getString(R.string.status_authenticating)
is State.Disconnecting -> connection_status_text.text = getString(R.string.status_disconnecting)
is State.Waiting -> connection_status_text.text = getString(R.string.status_waiting, state.seconds)
}
}
}
private val dismissStatus = {
connection_status_text.fadeOut()
}
private fun setupToolbar() { private fun setupToolbar() {
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.title_chats) (activity as AppCompatActivity).supportActionBar?.title = getString(R.string.title_chats)
} }
...@@ -107,7 +143,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -107,7 +143,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start), resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start),
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end))) resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)))
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
recycler_view.adapter = ChatRoomsAdapter(this) { chatRoom -> // TODO - use a ViewModel Mapper instead of using settings on the adapter
recycler_view.adapter = ChatRoomsAdapter(this,
settingsRepository.get(serverInteractor.get()!!)!!) { chatRoom ->
presenter.loadChatRoom(chatRoom) presenter.loadChatRoom(chatRoom)
} }
} }
......
...@@ -13,11 +13,15 @@ import android.support.v4.content.res.ResourcesCompat ...@@ -13,11 +13,15 @@ import android.support.v4.content.res.ResourcesCompat
import android.text.Layout import android.text.Layout
import android.text.Spannable import android.text.Spannable
import android.text.Spanned import android.text.Spanned
import android.text.TextUtils
import android.text.style.* import android.text.style.*
import android.util.Patterns import android.util.Patterns
import android.view.View import android.view.View
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import org.commonmark.node.AbstractVisitor import org.commonmark.node.AbstractVisitor
import org.commonmark.node.BlockQuote import org.commonmark.node.BlockQuote
import org.commonmark.node.Text import org.commonmark.node.Text
...@@ -47,7 +51,7 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -47,7 +51,7 @@ class MessageParser @Inject constructor(val context: Application, private val co
*/ */
fun renderMarkdown(text: String, quote: MessageViewModel? = null, selfUsername: String? = null): CharSequence { fun renderMarkdown(text: String, quote: MessageViewModel? = null, selfUsername: String? = null): CharSequence {
val builder = SpannableBuilder() val builder = SpannableBuilder()
val content = text val content = EmojiRepository.shortnameToUnicode(text, true)
val parentNode = parser.parse(toLenientMarkdown(content)) val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(QuoteMessageBodyVisitor(context, configuration, builder)) parentNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
quote?.apply { quote?.apply {
...@@ -58,6 +62,7 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -58,6 +62,7 @@ class MessageParser @Inject constructor(val context: Application, private val co
quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder)) quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
} }
parentNode.accept(LinkVisitor(builder)) parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(builder))
val result = builder.text() val result = builder.text()
applySpans(result, selfUsername) applySpans(result, selfUsername)
...@@ -139,6 +144,19 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -139,6 +144,19 @@ class MessageParser @Inject constructor(val context: Application, private val co
} }
} }
class EmojiVisitor(private val builder: SpannableBuilder) : AbstractVisitor() {
override fun visit(text: Text) {
val spannable = EmojiParser.parse(text.literal)
if (spannable is Spanned) {
val spans = spannable.getSpans(0, spannable.length, EmojiTypefaceSpan::class.java)
spans.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it), 0)
}
}
visitChildren(text)
}
}
class LinkVisitor(private val builder: SpannableBuilder) : AbstractVisitor() { class LinkVisitor(private val builder: SpannableBuilder) : AbstractVisitor() {
override fun visit(text: Text) { override fun visit(text: Text) {
......
...@@ -2,6 +2,7 @@ package chat.rocket.android.main.presentation ...@@ -2,6 +2,7 @@ package chat.rocket.android.main.presentation
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
...@@ -13,9 +14,11 @@ import javax.inject.Inject ...@@ -13,9 +14,11 @@ import javax.inject.Inject
class MainPresenter @Inject constructor(private val navigator: MainNavigator, class MainPresenter @Inject constructor(private val navigator: MainNavigator,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
managerFactory: ConnectionManagerFactory,
factory: RocketChatClientFactory) { factory: RocketChatClientFactory) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer)
fun toChatList() = navigator.toChatList() fun toChatList() = navigator.toChatList()
...@@ -49,4 +52,12 @@ class MainPresenter @Inject constructor(private val navigator: MainNavigator, ...@@ -49,4 +52,12 @@ class MainPresenter @Inject constructor(private val navigator: MainNavigator,
} }
localRepository.clearAllFromServer(currentServer) localRepository.clearAllFromServer(currentServer)
} }
fun connect() {
manager.connect()
}
fun disconnect() {
manager.disconnect()
}
} }
\ No newline at end of file
...@@ -29,6 +29,7 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector { ...@@ -29,6 +29,7 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
presenter.connect()
setupToolbar() setupToolbar()
setupNavigationView() setupNavigationView()
} }
...@@ -41,6 +42,13 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector { ...@@ -41,6 +42,13 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector {
} }
} }
override fun onDestroy() {
super.onDestroy()
if (isFinishing) {
presenter.disconnect()
}
}
override fun onLogout() { override fun onLogout() {
finish() finish()
val intent = Intent(this, AuthenticationActivity::class.java) val intent = Intent(this, AuthenticationActivity::class.java)
......
...@@ -48,30 +48,30 @@ const val ALLOW_MESSAGE_PINNING = "Message_AllowPinning" ...@@ -48,30 +48,30 @@ const val ALLOW_MESSAGE_PINNING = "Message_AllowPinning"
* If you need to access a Setting, add a const val key above, add it to the filter on * If you need to access a Setting, add a const val key above, add it to the filter on
* ServerPresenter.kt and a extension function to access it * ServerPresenter.kt and a extension function to access it
*/ */
fun Map<String, Value<Any>>.googleEnabled(): Boolean = this[ACCOUNT_GOOGLE]?.value == true fun PublicSettings.googleEnabled(): Boolean = this[ACCOUNT_GOOGLE]?.value == true
fun Map<String, Value<Any>>.facebookEnabled(): Boolean = this[ACCOUNT_FACEBOOK]?.value == true fun PublicSettings.facebookEnabled(): Boolean = this[ACCOUNT_FACEBOOK]?.value == true
fun Map<String, Value<Any>>.githubEnabled(): Boolean = this[ACCOUNT_GITHUB]?.value == true fun PublicSettings.githubEnabled(): Boolean = this[ACCOUNT_GITHUB]?.value == true
fun Map<String, Value<Any>>.linkedinEnabled(): Boolean = this[ACCOUNT_LINKEDIN]?.value == true fun PublicSettings.linkedinEnabled(): Boolean = this[ACCOUNT_LINKEDIN]?.value == true
fun Map<String, Value<Any>>.meteorEnabled(): Boolean = this[ACCOUNT_METEOR]?.value == true fun PublicSettings.meteorEnabled(): Boolean = this[ACCOUNT_METEOR]?.value == true
fun Map<String, Value<Any>>.twitterEnabled(): Boolean = this[ACCOUNT_TWITTER]?.value == true fun PublicSettings.twitterEnabled(): Boolean = this[ACCOUNT_TWITTER]?.value == true
fun Map<String, Value<Any>>.gitlabEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true fun PublicSettings.gitlabEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true
fun Map<String, Value<Any>>.wordpressEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true fun PublicSettings.wordpressEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun Map<String, Value<Any>>.useRealName(): Boolean = this[USE_REALNAME]?.value == true fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true
// Message settings // Message settings
fun Map<String, Value<Any>>.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true fun PublicSettings.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
fun Map<String, Value<Any>>.showEditedStatus(): Boolean = this[SHOW_EDITED_STATUS]?.value == true fun PublicSettings.showEditedStatus(): Boolean = this[SHOW_EDITED_STATUS]?.value == true
fun Map<String, Value<Any>>.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING]?.value == true fun PublicSettings.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING]?.value == true
fun Map<String, Value<Any>>.allowedMessageEditing(): Boolean = this[ALLOW_MESSAGE_EDITING]?.value == true fun PublicSettings.allowedMessageEditing(): Boolean = this[ALLOW_MESSAGE_EDITING]?.value == true
fun Map<String, Value<Any>>.allowedMessageDeleting(): Boolean = this[ALLOW_MESSAGE_DELETING]?.value == true fun PublicSettings.allowedMessageDeleting(): Boolean = this[ALLOW_MESSAGE_DELETING]?.value == true
fun Map<String, Value<Any>>.registrationEnabled(): Boolean { fun PublicSettings.registrationEnabled(): Boolean {
val value = this[ACCOUNT_REGISTRATION] val value = this[ACCOUNT_REGISTRATION]
return value?.value == "Public" return value?.value == "Public"
} }
fun Map<String, Value<Any>>.uploadMimeTypeFilter(): Array<String> { fun PublicSettings.uploadMimeTypeFilter(): Array<String> {
val values = this[UPLOAD_WHITELIST_MIMETYPES]?.value val values = this[UPLOAD_WHITELIST_MIMETYPES]?.value
values?.let { it as String }?.split(",")?.let { values?.let { it as String }?.split(",")?.let {
return it.mapToTypedArray { it.trim() } return it.mapToTypedArray { it.trim() }
...@@ -80,8 +80,8 @@ fun Map<String, Value<Any>>.uploadMimeTypeFilter(): Array<String> { ...@@ -80,8 +80,8 @@ fun Map<String, Value<Any>>.uploadMimeTypeFilter(): Array<String> {
return arrayOf("*/*") return arrayOf("*/*")
} }
fun Map<String, Value<Any>>.uploadMaxFileSize(): Int { fun PublicSettings.uploadMaxFileSize(): Int {
return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE
} }
fun Map<String, Value<Any>>.baseUrl() : String? = this[SITE_URL]?.value as String fun PublicSettings.baseUrl(): String? = this[SITE_URL]?.value as String
\ No newline at end of file \ No newline at end of file
package chat.rocket.android.server.infraestructure
import chat.rocket.common.model.BaseRoom
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.*
import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.Message
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
class ConnectionManager(internal val client: RocketChatClient) {
private val statusChannelList = ArrayList<Channel<State>>()
private val statusChannel = Channel<State>()
private var connectJob: Job? = null
private val roomAndSubscriptionChannels = ArrayList<Channel<StreamMessage<BaseRoom>>>()
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val subscriptionIdMap = HashMap<String, String>()
private var subscriptionId: String? = null
private var roomsId: String? = null
fun connect() {
if (connectJob?.isActive == true
&& (state !is State.Disconnected)) {
Timber.d("Already connected, just returning...")
return
}
// cleanup first
client.removeStateChannel(statusChannel)
client.disconnect()
connectJob?.cancel()
// Connect and setup
client.addStateChannel(statusChannel)
connectJob = launch {
for (status in statusChannel) {
Timber.d("Changing status to: $status")
when (status) {
is State.Connected -> {
client.subscribeSubscriptions { _, id ->
Timber.d("Subscribed to subscriptions: $id")
subscriptionId = id
}
client.subscribeRooms { _, id ->
Timber.d("Subscribed to rooms: $id")
roomsId = id
}
resubscribeRooms()
}
is State.Waiting -> {
Timber.d("Connection in: ${status.seconds}")
}
}
for (channel in statusChannelList) {
channel.send(status)
}
}
}
launch(parent = connectJob) {
for (room in client.roomsChannel) {
Timber.d("GOT Room streamed")
for (channel in roomAndSubscriptionChannels) {
channel.send(room)
}
}
}
launch(parent = connectJob) {
for (subscription in client.subscriptionsChannel) {
Timber.d("GOT Subscription streamed")
for (channel in roomAndSubscriptionChannels) {
channel.send(subscription)
}
}
}
launch(parent = connectJob) {
for (message in client.messagesChannel) {
Timber.d("Received new Message for room ${message.roomId}")
val channel = roomMessagesChannels[message.roomId]
channel?.send(message)
}
}
client.connect()
// Broadcast initial state...
val state = client.state
for (channel in statusChannelList) {
channel.offer(state)
}
}
private fun resubscribeRooms() {
roomMessagesChannels.toList().map { (roomId, channel) ->
client.subscribeRoomMessages(roomId) { _, id ->
Timber.d("Subscribed to $roomId: $id")
subscriptionIdMap[roomId] = id
}
}
}
fun disconnect() {
Timber.d("ConnectionManager DISCONNECT")
client.removeStateChannel(statusChannel)
client.disconnect()
connectJob?.cancel()
}
fun addStatusChannel(channel: Channel<State>) = statusChannelList.add(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 subscribeRoomMessages(roomId: String, channel: Channel<Message>) {
val oldSub = roomMessagesChannels.put(roomId, channel)
if (oldSub != null) {
Timber.d("Room $roomId already subscribed...")
return
}
if (client.state is State.Connected) {
client.subscribeRoomMessages(roomId) { _, id ->
Timber.d("Subscribed to $roomId: $id")
subscriptionIdMap[roomId] = id
}
}
}
fun unsubscribeRoomMessages(roomId: String) {
val sub = roomMessagesChannels.remove(roomId)
if (sub != null) {
val id = subscriptionIdMap.remove(roomId)
id?.let { client.unsubscribe(it) }
}
}
}
suspend fun ConnectionManager.chatRooms(timestamp: Long = 0, filterCustom: Boolean = true)
= client.chatRooms(timestamp, filterCustom)
val ConnectionManager.state: State
get() = client.state
\ No newline at end of file
package chat.rocket.android.server.infraestructure
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ConnectionManagerFactory @Inject constructor(private val factory: RocketChatClientFactory) {
private val cache = HashMap<String, ConnectionManager>()
fun create(url: String): ConnectionManager {
cache[url]?.let {
Timber.d("Returning CACHED Manager for: $url")
return it
}
Timber.d("Returning FRESH Manager for: $url")
val manager = ConnectionManager(factory.create(url))
cache[url] = manager
return manager
}
}
\ No newline at end of file
...@@ -28,7 +28,7 @@ class RocketChatClientFactory @Inject constructor(val okHttpClient: OkHttpClient ...@@ -28,7 +28,7 @@ class RocketChatClientFactory @Inject constructor(val okHttpClient: OkHttpClient
} }
Timber.d("Returning NEW client for: $url") Timber.d("Returning NEW client for: $url")
cache.put(url, client) cache[url] = client
return client return client
} }
} }
\ No newline at end of file
...@@ -2,19 +2,19 @@ package chat.rocket.android.server.infraestructure ...@@ -2,19 +2,19 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.LocalRepository.Companion.SETTINGS_KEY import chat.rocket.android.infrastructure.LocalRepository.Companion.SETTINGS_KEY
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.core.internal.SettingsAdapter import chat.rocket.core.internal.SettingsAdapter
import chat.rocket.core.model.Value
class SharedPreferencesSettingsRepository(private val localRepository: LocalRepository) : SettingsRepository { class SharedPreferencesSettingsRepository(private val localRepository: LocalRepository) : SettingsRepository {
private val adapter = SettingsAdapter().lenient() private val adapter = SettingsAdapter().lenient()
override fun save(url: String, settings: Map<String, Value<Any>>) { override fun save(url: String, settings: PublicSettings) {
localRepository.save("$SETTINGS_KEY$url", adapter.toJson(settings)) localRepository.save("$SETTINGS_KEY$url", adapter.toJson(settings))
} }
override fun get(url: String): Map<String, Value<Any>>? { override fun get(url: String): PublicSettings? {
val settings = localRepository.get("$SETTINGS_KEY$url") val settings = localRepository.get("$SETTINGS_KEY$url")
settings?.let { settings?.let {
return adapter.fromJson(it) return adapter.fromJson(it)
......
...@@ -2,7 +2,6 @@ package chat.rocket.android.util.extensions ...@@ -2,7 +2,6 @@ package chat.rocket.android.util.extensions
import android.view.View import android.view.View
import android.view.ViewAnimationUtils import android.view.ViewAnimationUtils
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
fun View.rotateBy(value: Float, duration: Long = 100) { fun View.rotateBy(value: Float, duration: Long = 100) {
...@@ -12,36 +11,59 @@ fun View.rotateBy(value: Float, duration: Long = 100) { ...@@ -12,36 +11,59 @@ fun View.rotateBy(value: Float, duration: Long = 100) {
.start() .start()
} }
fun View.fadeIn(startValue: Float, finishValue: Float, duration: Long = 200) { fun View.fadeIn(start: Float = 0f, end: Float = 1f, duration: Long = 200) {
animate() // already at end alpha, just set visible and return
.alpha(startValue) if (alpha == end) {
.setDuration(duration) setVisible(true)
.setInterpolator(DecelerateInterpolator()) return
.withEndAction({ }
animate()
.alpha(finishValue)
.setDuration(duration)
.setInterpolator(AccelerateInterpolator()).start()
}).start()
val animation = animate()
.alpha(end)
.setDuration(duration)
.setInterpolator(DecelerateInterpolator())
if (start != alpha) {
animate()
.alpha(start)
.setDuration(duration / 2) // half the time, so the entire animation runs on duration
.setInterpolator(DecelerateInterpolator())
.withEndAction {
animation.setDuration(duration / 2).start()
}.start()
} else {
animation.start()
}
setVisible(true) setVisible(true)
} }
fun View.fadeOut(startValue: Float, finishValue: Float, duration: Long = 200) { fun View.fadeOut(start: Float = 1f, end: Float = 0f, duration: Long = 200) {
animate() if (alpha == end) {
.alpha(startValue) setVisible(false)
.setDuration(duration) return
.setInterpolator(DecelerateInterpolator()) }
.withEndAction({
animate() val animation = animate()
.alpha(finishValue) .alpha(end)
.setDuration(duration) .setDuration(duration)
.setInterpolator(AccelerateInterpolator()).start() .setInterpolator(DecelerateInterpolator())
}).start() .withEndAction {
setVisible(false)
setVisible(false) }
if (start != alpha) {
animate()
.alpha(start)
.setDuration(duration / 2) // half the time, so the entire animation runs on duration
.setInterpolator(DecelerateInterpolator())
.withEndAction {
animation.setDuration(duration / 2).start()
}.start()
} else {
animation.start()
}
} }
fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float, endRadius: Float, duration: Long = 200) { fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float, endRadius: Float, duration: Long = 200) {
val anim = ViewAnimationUtils.createCircularReveal(this, centerX, centerY, startRadius, endRadius) val anim = ViewAnimationUtils.createCircularReveal(this, centerX, centerY, startRadius, endRadius)
anim.duration = duration anim.duration = duration
......
...@@ -6,6 +6,8 @@ import android.support.annotation.LayoutRes ...@@ -6,6 +6,8 @@ import android.support.annotation.LayoutRes
import android.support.annotation.StringRes import android.support.annotation.StringRes
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
...@@ -54,4 +56,13 @@ fun Activity.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = To ...@@ -54,4 +56,13 @@ fun Activity.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = To
fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration) fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration)
fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = activity!!.showToast(message, duration) fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = activity!!.showToast(message, duration)
\ No newline at end of file
fun RecyclerView.isAtBottom(): Boolean {
val manager: RecyclerView.LayoutManager? = layoutManager
if (manager is LinearLayoutManager) {
return manager.findFirstVisibleItemPosition() == 0
}
return false // or true??? we can't determine the first visible item.
}
\ No newline at end of file
...@@ -60,6 +60,7 @@ ...@@ -60,6 +60,7 @@
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:padding="8dp" android:padding="8dp"
android:visibility="invisible"
android:src="@drawable/ic_search_gray_24px" /> android:src="@drawable/ic_search_gray_24px" />
<ImageView <ImageView
...@@ -75,5 +76,4 @@ ...@@ -75,5 +76,4 @@
android:src="@drawable/ic_backspace_gray_24dp" /> android:src="@drawable/ic_backspace_gray_24dp" />
</RelativeLayout> </RelativeLayout>
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -53,4 +53,19 @@ ...@@ -53,4 +53,19 @@
android:layout_margin="5dp" android:layout_margin="5dp"
android:visibility="gone" /> android:visibility="gone" />
<TextView
android:id="@+id/connection_status_text"
android:layout_width="match_parent"
android:layout_height="32dp"
android:background="@color/colorPrimary"
android:elevation="4dp"
android:textColor="@color/white"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:visibility="gone"
android:alpha="0"
tools:alpha="1"
tools:visibility="visible"
tools:text="connected"/>
</RelativeLayout> </RelativeLayout>
...@@ -29,4 +29,19 @@ ...@@ -29,4 +29,19 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView
android:id="@+id/connection_status_text"
android:layout_width="match_parent"
android:layout_height="32dp"
android:background="@color/colorPrimary"
android:elevation="4dp"
android:textColor="@color/white"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:visibility="gone"
android:alpha="0"
tools:alpha="1"
tools:visibility="visible"
tools:text="connected"/>
</RelativeLayout> </RelativeLayout>
\ No newline at end of file
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
android:id="@+id/attachment_container" android:id="@+id/attachment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="72dp" android:paddingStart="72dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins" android:paddingEnd="@dimen/screen_edge_left_and_right_margins"
android:orientation="vertical"> android:orientation="vertical">
<com.facebook.drawee.view.SimpleDraweeView <com.facebook.drawee.view.SimpleDraweeView
......
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
android:id="@+id/url_preview_layout" android:id="@+id/url_preview_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="72dp" android:paddingStart="72dp"
android:layout_marginEnd="24dp"> android:paddingEnd="24dp">
<com.facebook.drawee.view.SimpleDraweeView <com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_preview" android:id="@+id/image_preview"
......
...@@ -88,4 +88,11 @@ ...@@ -88,4 +88,11 @@
<!-- Upload Messages --> <!-- Upload Messages -->
<string name="max_file_size_exceeded">Tamanho de arquivo (%1$d bytes) excedeu tamanho máximo de upload (%2$d bytes)</string> <string name="max_file_size_exceeded">Tamanho de arquivo (%1$d bytes) excedeu tamanho máximo de upload (%2$d bytes)</string>
<!-- Socket status -->
<string name="status_connected">conectado</string>
<string name="status_disconnected">desconetado</string>
<string name="status_connecting">conectando</string>
<string name="status_authenticating">autenticando</string>
<string name="status_disconnecting">desconectando</string>
<string name="status_waiting">conectando em %d segundos</string>
</resources> </resources>
\ No newline at end of file
...@@ -90,4 +90,12 @@ ...@@ -90,4 +90,12 @@
<!-- Upload Messages --> <!-- Upload Messages -->
<string name="max_file_size_exceeded">File size %1$d bytes exceeded max upload size of %2$d bytes</string> <string name="max_file_size_exceeded">File size %1$d bytes exceeded max upload size of %2$d bytes</string>
<!-- Socket status -->
<string name="status_connected">connected</string>
<string name="status_disconnected">disconnected</string>
<string name="status_connecting">connecting</string>
<string name="status_authenticating">authenticating</string>
<string name="status_disconnecting">disconnecting</string>
<string name="status_waiting">connecting in %d seconds</string>
</resources> </resources>
\ No newline at end of file
...@@ -11,6 +11,7 @@ ext { ...@@ -11,6 +11,7 @@ ext {
// Main dependencies // Main dependencies
support : '27.0.2', support : '27.0.2',
constraintLayout : '1.0.2', constraintLayout : '1.0.2',
androidKtx : '0.1',
dagger : '2.14.1', dagger : '2.14.1',
exoPlayer : '2.6.0', exoPlayer : '2.6.0',
playServices : '11.8.0', playServices : '11.8.0',
...@@ -49,6 +50,8 @@ ext { ...@@ -49,6 +50,8 @@ ext {
constraintLayout : "com.android.support.constraint:constraint-layout:${versions.constraintLayout}", constraintLayout : "com.android.support.constraint:constraint-layout:${versions.constraintLayout}",
cardView : "com.android.support:cardview-v7:${versions.support}", cardView : "com.android.support:cardview-v7:${versions.support}",
androidKtx : "androidx.core:core-ktx:${versions.androidKtx}",
dagger : "com.google.dagger:dagger:${versions.dagger}", dagger : "com.google.dagger:dagger:${versions.dagger}",
daggerSupport : "com.google.dagger:dagger-android-support:${versions.dagger}", daggerSupport : "com.google.dagger:dagger-android-support:${versions.dagger}",
daggerProcessor : "com.google.dagger:dagger-compiler:${versions.dagger}", daggerProcessor : "com.google.dagger:dagger-compiler:${versions.dagger}",
......
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