Unverified Commit c1871a62 authored by Lucio Maciel's avatar Lucio Maciel Committed by GitHub

Merge pull request #796 from RocketChat/release/2.0.0-beta7

[RELEASE] 2.0.0 beta 7
parents fd036b3c a639df30
...@@ -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 1011
versionName "2.0.0-dev7" versionName "2.0.0-dev9"
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
......
...@@ -7,14 +7,14 @@ import chat.rocket.android.helper.NetworkHelper ...@@ -7,14 +7,14 @@ import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
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.RocketChatClientFactory
import chat.rocket.android.util.extensions.isEmailValid
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.RocketChatTwoFactorException import chat.rocket.common.RocketChatTwoFactorException
import chat.rocket.common.model.Token
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login import chat.rocket.core.internal.rest.*
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken
import javax.inject.Inject import javax.inject.Inject
class LoginPresenter @Inject constructor(private val view: LoginView, class LoginPresenter @Inject constructor(private val view: LoginView,
...@@ -93,15 +93,31 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -93,15 +93,31 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
view.showLoading() view.showLoading()
try { try {
val token = client.login(usernameOrEmail, password) var token: Token? = null
val me = client.me() if (usernameOrEmail.isEmailValid()) {
multiServerRepository.save( token = client.loginWithEmail(usernameOrEmail, password)
server, } else {
TokenModel(token.userId, token.authToken) val settings = settingsInteractor.get(server)
) if (settings != null) {
localRepository.save(LocalRepository.USERNAME_KEY, me.username) token = if (settings.ldapEnabled()) {
registerPushToken() client.loginWithLdap(usernameOrEmail, password)
navigator.toChatList() } else {
client.login(usernameOrEmail, password)
}
} else {
navigator.toServerScreen()
}
}
if (token != null) {
val me = client.me()
multiServerRepository.save(server, TokenModel(token.userId, token.authToken))
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
registerPushToken()
navigator.toChatList()
} else {
view.showGenericErrorMessage()
}
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
when (exception) { when (exception) {
is RocketChatTwoFactorException -> { is RocketChatTwoFactorException -> {
......
...@@ -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)
} }
} }
......
package chat.rocket.android.chatroom.di package chat.rocket.android.chatroom.di
import android.arch.lifecycle.LifecycleOwner import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.chatroom.presentation.ChatRoomView import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.ChatRoomFragment import chat.rocket.android.chatroom.ui.ChatRoomFragment
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
...@@ -13,6 +15,9 @@ import kotlinx.coroutines.experimental.Job ...@@ -13,6 +15,9 @@ import kotlinx.coroutines.experimental.Job
@PerFragment @PerFragment
class ChatRoomFragmentModule { class ChatRoomFragmentModule {
@Provides
fun provideChatRoomNavigator(activity: ChatRoomActivity) = ChatRoomNavigator(activity)
@Provides @Provides
fun chatRoomView(frag: ChatRoomFragment): ChatRoomView { fun chatRoomView(frag: ChatRoomFragment): ChatRoomView {
return frag return frag
......
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.members.ui.newInstance
import chat.rocket.android.util.extensions.addFragmentBackStack
class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
fun toMembersList(chatRoomId: String, chatRoomType: String) {
activity.addFragmentBackStack("MembersFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomType)
}
}
}
\ No newline at end of file
...@@ -6,41 +6,50 @@ import chat.rocket.android.chatroom.domain.UriInteractor ...@@ -6,41 +6,50 @@ 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
class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val navigator: ChatRoomNavigator,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
getSettingsInteractor: GetSettingsInteractor, getSettingsInteractor: GetSettingsInteractor,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
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 {
...@@ -55,10 +64,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -55,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 {
...@@ -69,6 +75,10 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -69,6 +75,10 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} finally { } finally {
view.hideLoading() view.hideLoading()
} }
if (offset == 0L) {
subscribeState()
}
} }
} }
...@@ -131,7 +141,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -131,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)
...@@ -142,57 +152,70 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -142,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) {
is State.Authenticating -> Timber.d("Authenticating") launch(UI) {
is State.Connected -> { view.showConnectionState(state)
Timber.d("Connected")
subId = client.subscribeRoomMessages(roomId) { success, _ ->
Timber.d("subscribe messages for $roomId: $success")
}
} }
}
}
Timber.d("Done on statusChannel")
}
when (client.state) { if (state is State.Connected) {
is State.Connected -> { loadMissingMessages()
Timber.d("Already connected") }
subId = client.subscribeRoomMessages(roomId) { success, _ ->
Timber.d("subscribe messages for $roomId: $success")
} }
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.
* *
...@@ -317,16 +340,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -317,16 +340,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} }
} }
private suspend fun listenMessages(roomId: String) { fun toMembersList(chatRoomId: String, chatRoomType: String) = navigator.toMembersList(chatRoomId, chatRoomType)
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) {
......
...@@ -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 {
......
...@@ -8,9 +8,12 @@ import android.os.Bundle ...@@ -8,9 +8,12 @@ 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 chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
...@@ -35,6 +38,11 @@ private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only" ...@@ -35,6 +38,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
...@@ -45,6 +53,9 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -45,6 +53,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" }
...@@ -57,7 +68,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -57,7 +68,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, true) isChatRoomReadOnly = intent.getBooleanExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, true)
requireNotNull(chatRoomType) { "no is_chat_room_read_only provided in Intent extras" } requireNotNull(chatRoomType) { "no is_chat_room_read_only provided in Intent extras" }
setupToolbar(chatRoomName) setupToolbar()
addFragment("ChatRoomFragment", R.id.fragment_container) { addFragment("ChatRoomFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly) newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly)
...@@ -72,22 +83,23 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -72,22 +83,23 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector return fragmentDispatchingAndroidInjector
} }
private fun setupToolbar(chatRoomName: String) { private fun setupToolbar() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName text_room_name.textContent = chatRoomName
var drawable: Drawable? = null val roomType = roomTypeOf(chatRoomType)
when (chatRoomType) { val drawable = when (roomType) {
RoomType.CHANNEL.toString() -> { is RoomType.Channel -> {
drawable = DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, this) DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, this)
} }
RoomType.PRIVATE_GROUP.toString() -> { is RoomType.PrivateGroup -> {
drawable = DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, this) DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, this)
} }
RoomType.DIRECT_MESSAGE.toString() -> { is RoomType.DirectMessage -> {
drawable = DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, this) DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, this)
} }
else -> null
} }
drawable?.let { drawable?.let {
...@@ -102,6 +114,10 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -102,6 +114,10 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
} }
} }
fun setupToolbarTitle(toolbarTitle: String) {
text_room_name.textContent = toolbarTitle
}
private fun finishActivity() { private fun finishActivity() {
super.onBackPressed() super.onBackPressed()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit) overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
......
...@@ -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.*
...@@ -98,6 +99,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -98,6 +99,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupToolbar(chatRoomName)
presenter.loadMessages(chatRoomId, chatRoomType) presenter.loadMessages(chatRoomId, chatRoomType)
setupRecyclerView() setupRecyclerView()
...@@ -112,7 +115,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -112,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()
...@@ -133,6 +136,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -133,6 +136,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_members_list -> {
presenter.toMembersList(chatRoomId, chatRoomType)
}
R.id.action_pinned_messages -> { R.id.action_pinned_messages -> {
val intent = Intent(activity, PinnedMessagesActivity::class.java).apply { val intent = Intent(activity, PinnedMessagesActivity::class.java).apply {
putExtra(BUNDLE_CHAT_ROOM_ID, chatRoomId) putExtra(BUNDLE_CHAT_ROOM_ID, chatRoomId)
...@@ -220,6 +226,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -220,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)
}
}
} }
} }
...@@ -247,6 +259,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -247,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)
} }
} }
...@@ -283,6 +296,28 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -283,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) {
...@@ -310,11 +345,21 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -310,11 +345,21 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
text_room_is_read_only.setVisible(true) text_room_is_read_only.setVisible(true)
input_container.setVisible(false) input_container.setVisible(false)
} else { } else {
button_send.alpha = 0f
button_send.setVisible(false)
button_show_attachment_options.alpha = 1f
button_show_attachment_options.setVisible(true)
subscribeTextMessage() subscribeTextMessage()
emojiKeyboardPopup = EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container)) emojiKeyboardPopup = EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
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() {
...@@ -390,6 +435,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -390,6 +435,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)
}) })
} }
...@@ -401,9 +447,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -401,9 +447,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
} }
private fun unsubscribeTextMessage() { private fun unsubscribeTextMessage() {
if (!compositeDisposable.isDisposed) { compositeDisposable.clear()
compositeDisposable.dispose()
}
} }
private fun setupComposeMessageButtons(charSequence: CharSequence) { private fun setupComposeMessageButtons(charSequence: CharSequence) {
...@@ -435,4 +479,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -435,4 +479,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
view_dim.setVisible(false) view_dim.setVisible(false)
} }
private fun setupToolbar(toolbarTitle: String) {
(activity as ChatRoomActivity).setupToolbarTitle(toolbarTitle)
}
} }
\ No newline at end of file
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) {
is State.Authenticating -> Timber.d("Authenticating") launch(UI) {
is State.Connected -> { view.showConnectionState(state)
Timber.d("Connected")
client.subscribeSubscriptions { success, _ ->
Timber.d("subscriptions: $success")
}
client.subscribeRooms { success, _ ->
Timber.d("rooms: $success")
}
} }
}
}
Timber.d("Done on statusChannel")
}
when (client.state) { if (state is State.Connected) {
is 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
...@@ -11,6 +11,8 @@ import android.view.ViewGroup ...@@ -11,6 +11,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
...@@ -23,6 +25,7 @@ import kotlinx.android.synthetic.main.item_chat.view.* ...@@ -23,6 +25,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()
...@@ -73,27 +76,27 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -73,27 +76,27 @@ class ChatRoomsAdapter(private val context: Context,
private fun bindName(chatRoom: ChatRoom, textView: TextView) { private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.textContent = chatRoom.name textView.textContent = chatRoom.name
var drawable: Drawable? = null var drawable = when (chatRoom.type) {
when (chatRoom.type) {
is RoomType.Channel -> { is RoomType.Channel -> {
drawable = DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, context) DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, context)
} }
is RoomType.PrivateGroup -> { is RoomType.PrivateGroup -> {
drawable = DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, context) DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, context)
} }
is RoomType.DirectMessage -> { is RoomType.DirectMessage -> {
drawable = DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, context) DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, context)
} }
else -> null
} }
drawable?.let { drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it) val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate() val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, context, val color = when (chatRoom.alert || chatRoom.unread > 0) {
when (chatRoom.alert || chatRoom.unread > 0) { true -> R.color.colorPrimaryText
true -> R.color.colorPrimaryText false -> R.color.colorSecondaryText
false -> R.color.colorSecondaryText }
}) DrawableHelper.tintDrawable(mutableDrawable, context, color)
DrawableHelper.compoundDrawable(textView, mutableDrawable) DrawableHelper.compoundDrawable(textView, mutableDrawable)
} }
} }
......
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()
} }
...@@ -53,6 +62,11 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -53,6 +62,11 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
presenter.loadChatRooms() presenter.loadChatRooms()
} }
override fun onDestroyView() {
listJob?.cancel()
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.chatrooms, menu) inflater.inflate(R.menu.chatrooms, menu)
...@@ -71,15 +85,20 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -71,15 +85,20 @@ 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 +115,28 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -96,6 +115,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 +148,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -107,7 +148,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)
} }
} }
......
...@@ -16,6 +16,7 @@ import chat.rocket.android.dagger.scope.PerActivity ...@@ -16,6 +16,7 @@ import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.main.di.MainActivityProvider import chat.rocket.android.main.di.MainActivityProvider
import chat.rocket.android.main.di.MainModule import chat.rocket.android.main.di.MainModule
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.members.di.MembersFragmentProvider
import chat.rocket.android.profile.di.ProfileFragmentProvider import chat.rocket.android.profile.di.ProfileFragmentProvider
import chat.rocket.android.settings.password.di.PasswordFragmentProvider import chat.rocket.android.settings.password.di.PasswordFragmentProvider
import chat.rocket.android.settings.password.ui.PasswordActivity import chat.rocket.android.settings.password.ui.PasswordActivity
...@@ -44,7 +45,7 @@ abstract class ActivityBuilder { ...@@ -44,7 +45,7 @@ abstract class ActivityBuilder {
abstract fun bindMainActivity(): MainActivity abstract fun bindMainActivity(): MainActivity
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [ChatRoomFragmentProvider::class]) @ContributesAndroidInjector(modules = [ChatRoomFragmentProvider::class, MembersFragmentProvider::class])
abstract fun bindChatRoomActivity(): ChatRoomActivity abstract fun bindChatRoomActivity(): ChatRoomActivity
@PerActivity @PerActivity
......
...@@ -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()
...@@ -51,4 +54,12 @@ class MainPresenter @Inject constructor(private val navigator: MainNavigator, ...@@ -51,4 +54,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
...@@ -35,6 +35,7 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector { ...@@ -35,6 +35,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()
} }
...@@ -47,6 +48,13 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector { ...@@ -47,6 +48,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)
......
package chat.rocket.android.member.ui
import android.os.Bundle
import android.support.design.widget.BottomSheetDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import kotlinx.android.synthetic.main.fragment_member_bottom_sheet.*
fun newInstance(avatarUri: String, realName: String, username: String, email: String, utcOffset: String): BottomSheetDialogFragment {
return MemberBottomSheetFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_AVATAR_URI, avatarUri)
putString(BUNDLE_REAL_NAME, realName)
putString(BUNDLE_USERNAME, username)
putString(BUNDLE_EMAIL, email)
putString(BUNDLE_UTC_OFFSET, utcOffset)
}
}
}
private const val BUNDLE_AVATAR_URI = "avatar_uri"
private const val BUNDLE_REAL_NAME = "real_name"
private const val BUNDLE_USERNAME = "username"
private const val BUNDLE_EMAIL = "email"
private const val BUNDLE_UTC_OFFSET = "utc_offset"
class MemberBottomSheetFragment: BottomSheetDialogFragment() {
private lateinit var avatarUri: String
private lateinit var realName: String
private lateinit var username: String
private lateinit var email: String
private lateinit var utcOffset: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val bundle = arguments
if (bundle != null) {
avatarUri = bundle.getString(BUNDLE_AVATAR_URI)
realName = bundle.getString(BUNDLE_REAL_NAME)
username = bundle.getString(BUNDLE_USERNAME)
email = bundle.getString(BUNDLE_EMAIL)
utcOffset = bundle.getString(BUNDLE_UTC_OFFSET)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_member_bottom_sheet, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showMemberDetails()
}
private fun showMemberDetails() {
image_bottom_sheet_avatar.setImageURI(avatarUri)
text_bottom_sheet_member_name.content = realName
text_bottom_sheet_member_username.content = username
if (email.isNotEmpty()) {
text_member_email_address.textContent = email
} else {
text_email_address.setVisible(false)
text_member_email_address.setVisible(false)
}
if (utcOffset.isNotEmpty()){
text_member_utc.content = utcOffset
} else {
text_utc.setVisible(false)
text_member_utc.setVisible(false)
}
}
}
\ No newline at end of file
package chat.rocket.android.members.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_member.view.*
class MembersAdapter(private val listener: (MemberViewModel) -> Unit) : RecyclerView.Adapter<MembersAdapter.ViewHolder>() {
private var dataSet: List<MemberViewModel> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MembersAdapter.ViewHolder = ViewHolder(parent.inflate(R.layout.item_member))
override fun onBindViewHolder(holder: MembersAdapter.ViewHolder, position: Int) = holder.bind(dataSet[position], listener)
override fun getItemCount(): Int = dataSet.size
fun prependData(dataSet: List<MemberViewModel>) {
this.dataSet = dataSet
notifyItemRangeInserted(0, dataSet.size)
}
fun appendData(dataSet: List<MemberViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet += dataSet
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(memberViewModel: MemberViewModel, listener: (MemberViewModel) -> Unit) = with(itemView) {
image_avatar.setImageURI(memberViewModel.avatarUri)
text_member.content = memberViewModel.displayName
setOnClickListener { listener(memberViewModel) }
}
}
}
\ No newline at end of file
package chat.rocket.android.members.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.members.presentation.MembersNavigator
import chat.rocket.android.members.presentation.MembersView
import chat.rocket.android.members.ui.MembersFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class MembersFragmentModule {
@Provides
fun provideChatRoomNavigator(activity: ChatRoomActivity) = MembersNavigator(activity)
@Provides
fun membersView(frag: MembersFragment): MembersView {
return frag
}
@Provides
fun provideLifecycleOwner(frag: MembersFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
\ No newline at end of file
package chat.rocket.android.members.di
import chat.rocket.android.members.ui.MembersFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class MembersFragmentProvider {
@ContributesAndroidInjector(modules = [MembersFragmentModule::class])
abstract fun provideMembersFragment(): MembersFragment
}
\ No newline at end of file
package chat.rocket.android.members.presentation
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.member.ui.newInstance
class MembersNavigator(internal val activity: ChatRoomActivity) {
fun toMemberDetails(avatarUri: String, realName: String, username: String, email: String, utcOffset: String) {
activity.apply {
newInstance(avatarUri, realName, username, email, utcOffset)
.show(supportFragmentManager, "MemberBottomSheetFragment")
}
}
}
package chat.rocket.android.members.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.members.viewmodel.MemberViewModelMapper
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.getMembers
import javax.inject.Inject
class MembersPresenter @Inject constructor(private val view: MembersView,
private val navigator: MembersNavigator,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory,
private val mapper: MemberViewModelMapper) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
fun loadChatRoomsMembers(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
launchUI(strategy) {
try {
view.showLoading()
val members = client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 60)
val memberViewModels = mapper.mapToViewModelList(members.result)
view.showMembers(memberViewModels, members.total)
} catch (ex: RocketChatException) {
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
}
}
fun toMemberDetails(memberViewModel: MemberViewModel) {
val avatarUri = memberViewModel.avatarUri.toString()
val realName = memberViewModel.realName.toString()
val username = "@${memberViewModel.username}"
val email = memberViewModel.email ?: ""
val utcOffset = memberViewModel.utcOffset.toString()
navigator.toMemberDetails(avatarUri, realName, username, email, utcOffset)
}
}
\ No newline at end of file
package chat.rocket.android.members.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.members.viewmodel.MemberViewModel
interface MembersView: LoadingView, MessageView {
/**
* Shows a list of members of a room.
*
* @param dataSet The data set to show.
* @param total The total number of members.
*/
fun showMembers(dataSet: List<MemberViewModel>, total: Long)
}
\ No newline at end of file
package chat.rocket.android.members.ui
import android.os.Bundle
import android.support.v4.app.Fragment
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.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.members.adapter.MembersAdapter
import chat.rocket.android.members.presentation.MembersPresenter
import chat.rocket.android.members.presentation.MembersView
import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.widget.DividerItemDecoration
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_members.*
import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment {
return MembersFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
}
}
}
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
class MembersFragment : Fragment(), MembersView {
@Inject lateinit var presenter: MembersPresenter
private val adapter: MembersAdapter = MembersAdapter { memberViewModel -> presenter.toMemberDetails(memberViewModel) }
private val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_members)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as AppCompatActivity).supportActionBar?.title = ""
setupRecyclerView()
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType)
}
override fun showMembers(dataSet: List<MemberViewModel>, total: Long) {
activity?.apply {
setupToolbar(total)
if (adapter.itemCount == 0) {
adapter.prependData(dataSet)
if (dataSet.size >= 59) { // TODO Check why the API retorns the specified count -1
recycler_view.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType, page * 60L)
}
})
}
} else {
adapter.appendData(dataSet)
}
}
}
override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
private fun setupRecyclerView() {
activity?.apply {
recycler_view.layoutManager = linearLayoutManager
recycler_view.addItemDecoration(DividerItemDecoration(this))
recycler_view.adapter = adapter
}
}
private fun setupToolbar(totalMembers: Long) {
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_members, totalMembers))
}
}
\ No newline at end of file
package chat.rocket.android.members.viewmodel
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.useRealName
import chat.rocket.common.model.User
import chat.rocket.core.model.Value
class MemberViewModel(private val member: User, private val settings: Map<String, Value<Any>>, private val baseUrl: String?) {
val avatarUri: String?
val displayName: String
val realName: String?
val username: String?
val email: String?
val utcOffset: Float?
init {
avatarUri = getUserAvatar()
displayName = getUserDisplayName()
realName = getUserRealName()
username = getUserUsername()
email = getUserEmail()
utcOffset = getUserUtcOffset()
}
private fun getUserAvatar(): String? {
val username = member.username ?: "?"
return baseUrl?.let {
UrlHelper.getAvatarUrl(baseUrl, username)
}
}
private fun getUserDisplayName(): String {
val username = member.username
val realName = member.name
val senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString()
}
private fun getUserRealName(): String? = member.name
private fun getUserUsername(): String? = member.username
private fun getUserEmail(): String? = member.emails?.get(0)?.address
private fun getUserUtcOffset(): Float? = member.utcOffset
}
\ No newline at end of file
package chat.rocket.android.members.viewmodel
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.baseUrl
import chat.rocket.common.model.User
import chat.rocket.core.model.Value
import javax.inject.Inject
class MemberViewModelMapper @Inject constructor(serverInteractor: GetCurrentServerInteractor, getSettingsInteractor: GetSettingsInteractor) {
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private val baseUrl = settings.baseUrl()
fun mapToViewModelList(memberList: List<User>): List<MemberViewModel> {
return memberList.map { MemberViewModel(it, settings, baseUrl) }
}
}
\ No newline at end of file
...@@ -48,30 +48,31 @@ const val ALLOW_MESSAGE_PINNING = "Message_AllowPinning" ...@@ -48,30 +48,31 @@ 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
fun PublicSettings.ldapEnabled(): Boolean = this[LDAP_ENABLE]?.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 +81,8 @@ fun Map<String, Value<Any>>.uploadMimeTypeFilter(): Array<String> { ...@@ -80,8 +81,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)
......
...@@ -12,7 +12,12 @@ fun View.rotateBy(value: Float, duration: Long = 100) { ...@@ -12,7 +12,12 @@ fun View.rotateBy(value: Float, duration: Long = 100) {
.start() .start()
} }
fun View.fadeIn(startValue: Float, finishValue: Float, duration: Long = 200) { fun View.fadeIn(startValue: Float = 0f, finishValue: Float = 1f, duration: Long = 200) {
if (alpha == finishValue) {
setVisible(true)
return
}
animate() animate()
.alpha(startValue) .alpha(startValue)
.setDuration(duration) .setDuration(duration)
...@@ -27,7 +32,12 @@ fun View.fadeIn(startValue: Float, finishValue: Float, duration: Long = 200) { ...@@ -27,7 +32,12 @@ fun View.fadeIn(startValue: Float, finishValue: Float, duration: Long = 200) {
setVisible(true) setVisible(true)
} }
fun View.fadeOut(startValue: Float, finishValue: Float, duration: Long = 200) { fun View.fadeOut(startValue: Float = 1f, finishValue: Float = 0f, duration: Long = 200) {
if (alpha == finishValue) {
setVisible(false)
return
}
animate() animate()
.alpha(startValue) .alpha(startValue)
.setDuration(duration) .setDuration(duration)
......
...@@ -3,6 +3,7 @@ package chat.rocket.android.util.extensions ...@@ -3,6 +3,7 @@ package chat.rocket.android.util.extensions
import android.text.Spannable import android.text.Spannable
import android.text.Spanned import android.text.Spanned
import android.text.TextUtils import android.text.TextUtils
import android.util.Patterns
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
...@@ -31,6 +32,8 @@ fun EditText.erase() { ...@@ -31,6 +32,8 @@ fun EditText.erase() {
} }
} }
fun String.isEmailValid(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
var TextView.textContent: String var TextView.textContent: String
get() = text.toString() get() = text.toString()
set(value) { set(value) {
......
...@@ -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
...@@ -19,58 +19,98 @@ ...@@ -19,58 +19,98 @@
android:strokeWidth="1" /> android:strokeWidth="1" />
<path <path
android:fillColor="#FFFFFF" android:fillAlpha="1"
android:fillType="nonZero" android:fillColor="#ffffff"
android:pathData="M121.13,18.01L119.97,21.57L117.68,28.64C117.56,29 117.04,29 116.92,28.64L114.63,21.57L107,21.57L104.71,28.64C104.59,29 104.08,29 103.96,28.64L101.66,21.57L100.5,18.01C100.4,17.68 100.51,17.33 100.79,17.12L110.82,9.84L120.84,17.12C121.12,17.33 121.23,17.68 121.13,18.01" android:pathData="M122.13,22.99L120.97,19.43C119.6,15.19 118.83,12.83 118.68,12.36C118.56,12 118.04,12 117.92,12.36C117.77,12.83 117,15.19 115.63,19.43L108,19.43C106.63,15.19 105.86,12.83 105.71,12.36C105.59,12 105.08,12 104.96,12.36C104.81,12.83 104.04,15.19 102.66,19.43C101.96,21.57 101.58,22.75 101.5,22.99C101.4,23.32 101.51,23.67 101.79,23.88C102.46,24.37 105.8,26.79 111.82,31.16C117.83,26.79 121.17,24.37 121.84,23.88C122.12,23.67 122.23,23.32 122.13,22.99" />
android:strokeColor="#00000000" <path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M122.13,22.99L120.97,19.43C119.6,15.19 118.83,12.83 118.68,12.36C118.56,12 118.04,12 117.92,12.36C117.77,12.83 117,15.19 115.63,19.43L108,19.43C106.63,15.19 105.86,12.83 105.71,12.36C105.59,12 105.08,12 104.96,12.36C104.81,12.83 104.04,15.19 102.66,19.43C101.96,21.57 101.58,22.75 101.5,22.99C101.4,23.32 101.51,23.67 101.79,23.88C102.46,24.37 105.8,26.79 111.82,31.16C117.83,26.79 121.17,24.37 121.84,23.88C122.12,23.67 122.23,23.32 122.13,22.99"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" /> android:strokeWidth="1" />
<path <path
android:fillColor="#FFFFFF" android:fillAlpha="1"
android:fillType="nonZero" android:fillColor="#ffffff"
android:pathData="M110.82,9.84l0,0l3.81,11.73l-7.62,0z" android:pathData="M111.82,31.16L111.82,31.16L115.63,19.43L108.01,19.43L111.82,31.16Z" />
android:strokeColor="#00000000" <path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M111.82,31.16L111.82,31.16L115.63,19.43L108.01,19.43L111.82,31.16Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" /> android:strokeWidth="1" />
<path <path
android:fillColor="#FFFFFF" android:fillAlpha="1"
android:fillType="nonZero" android:fillColor="#ffffff"
android:pathData="M110.82,9.84l-3.81,11.73l-5.34,0z" android:pathData="M111.82,31.16L108.01,19.43L102.67,19.43L111.82,31.16Z" />
android:strokeColor="#00000000" <path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M111.82,31.16L108.01,19.43L102.67,19.43L111.82,31.16Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" /> android:strokeWidth="1" />
<path <path
android:fillColor="#FFFFFF" android:fillAlpha="1"
android:fillType="nonZero" android:fillColor="#ffffff"
android:pathData="M101.66,21.57L101.66,21.57L100.5,18.01C100.4,17.68 100.51,17.33 100.79,17.12L110.82,9.84L101.66,21.57L101.66,21.57Z" android:pathData="M102.66,19.43C101.96,21.57 101.58,22.75 101.5,22.99C101.4,23.32 101.51,23.67 101.79,23.88C102.46,24.37 105.8,26.79 111.82,31.16L102.66,19.43L102.66,19.43L102.66,19.43Z" />
android:strokeColor="#00000000" <path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M102.66,19.43C101.96,21.57 101.58,22.75 101.5,22.99C101.4,23.32 101.51,23.67 101.79,23.88C102.46,24.37 105.8,26.79 111.82,31.16L102.66,19.43L102.66,19.43L102.66,19.43Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" /> android:strokeWidth="1" />
<path <path
android:fillColor="#FFFFFF" android:fillAlpha="1"
android:fillType="nonZero" android:fillColor="#ffffff"
android:pathData="M101.66,21.57L107,21.57L104.71,28.64C104.59,29 104.08,29 103.96,28.64L101.66,21.57L101.66,21.57Z" android:pathData="M108,19.43C106.63,15.19 105.86,12.83 105.71,12.36C105.59,12 105.08,12 104.96,12.36C104.81,12.83 104.04,15.19 102.66,19.43L102.66,19.43L108,19.43Z" />
android:strokeColor="#00000000" <path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M108,19.43C106.63,15.19 105.86,12.83 105.71,12.36C105.59,12 105.08,12 104.96,12.36C104.81,12.83 104.04,15.19 102.66,19.43L102.66,19.43L108,19.43Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" /> android:strokeWidth="1" />
<path <path
android:fillColor="#FFFFFF" android:fillAlpha="1"
android:fillType="nonZero" android:fillColor="#ffffff"
android:pathData="M110.82,9.84l3.81,11.73l5.34,0z" android:pathData="M111.82,31.16L115.63,19.43L120.97,19.43L111.82,31.16Z" />
android:strokeColor="#00000000" <path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M111.82,31.16L115.63,19.43L120.97,19.43L111.82,31.16Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" /> android:strokeWidth="1" />
<path <path
android:fillColor="#FFFFFF" android:fillAlpha="1"
android:fillType="nonZero" android:fillColor="#ffffff"
android:pathData="M119.97,21.57L119.97,21.57L121.13,18.01C121.24,17.68 121.12,17.33 120.84,17.12L110.82,9.84L119.97,21.57L119.97,21.57Z" android:pathData="M120.97,19.43C121.67,21.57 122.05,22.75 122.13,22.99C122.24,23.32 122.12,23.67 121.84,23.88C121.17,24.37 117.83,26.79 111.82,31.16L120.97,19.43L120.97,19.43L120.97,19.43Z" />
android:strokeColor="#00000000" <path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M120.97,19.43C121.67,21.57 122.05,22.75 122.13,22.99C122.24,23.32 122.12,23.67 121.84,23.88C121.17,24.37 117.83,26.79 111.82,31.16L120.97,19.43L120.97,19.43L120.97,19.43Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" /> android:strokeWidth="1" />
<path <path
android:fillColor="#FFFFFF" android:fillAlpha="1"
android:fillType="nonZero" android:fillColor="#ffffff"
android:pathData="M119.97,21.57L114.63,21.57L116.92,28.64C117.04,29 117.56,29 117.68,28.64L119.97,21.57L119.97,21.57Z" android:pathData="M115.63,19.43C117,15.19 117.77,12.83 117.92,12.36C118.04,12 118.56,12 118.68,12.36C118.83,12.83 119.6,15.19 120.97,19.43L120.97,19.43L115.63,19.43Z" />
android:strokeColor="#00000000" <path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M115.63,19.43C117,15.19 117.77,12.83 117.92,12.36C118.04,12 118.56,12 118.68,12.36C118.83,12.83 119.6,15.19 120.97,19.43L120.97,19.43L115.63,19.43Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" /> android:strokeWidth="1" />
</vector> </vector>
\ 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
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
android:cursorVisible="false" android:cursorVisible="false"
android:hint="@string/default_server" android:hint="@string/default_server"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:digits="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-/:" android:digits="0123456789abcdefghijklmnopqrstuvwxyz.-/:"
android:inputType="textUri" android:inputType="textUri"
android:paddingEnd="0dp" android:paddingEnd="0dp"
android:paddingStart="0dp" /> android:paddingStart="0dp" />
...@@ -52,4 +52,4 @@ ...@@ -52,4 +52,4 @@
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:text="@string/action_connect" /> android:text="@string/action_connect" />
</RelativeLayout> </RelativeLayout>
\ No newline at end of file
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout" android:id="@+id/root_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
tools:context=".chatroom.ui.ChatRoomFragment">
<com.wang.avi.AVLoadingIndicatorView <com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading" android:id="@+id/view_loading"
...@@ -53,4 +54,19 @@ ...@@ -53,4 +54,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
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/member_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="16dp"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_bottom_sheet_avatar"
android:layout_width="match_parent"
android:layout_height="200dp" />
<LinearLayout
android:id="@+id/name_and_username_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorBackgroundMemberContainer"
android:orientation="vertical"
android:paddingBottom="10dp"
android:paddingStart="16dp"
android:paddingTop="10dp"
app:layout_constraintBottom_toBottomOf="@+id/image_bottom_sheet_avatar"
app:layout_constraintLeft_toLeftOf="parent">
<TextView
android:id="@+id/text_bottom_sheet_member_name"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
tools:text="Ronald Perkins" />
<TextView
android:id="@+id/text_bottom_sheet_member_username"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="@color/white"
tools:text="\@ronaldPerkins" />
</LinearLayout>
<TextView
android:id="@+id/text_email_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/msg_email_address"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image_bottom_sheet_avatar" />
<TextView
android:id="@+id/text_member_email_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="10dp"
android:textColor="@color/colorPrimaryText"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_email_address"
tools:text="ronald@perkins.com" />
<TextView
android:id="@+id/text_utc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/msg_utc_offset"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_member_email_address" />
<TextView
android:id="@+id/text_member_utc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="10dp"
android:textColor="@color/colorPrimaryText"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_utc"
tools:text="+01:00" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".members.ui.MembersFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator" />
</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
<EditText <EditText
android:id="@+id/text_avatar_url" android:id="@+id/text_avatar_url"
style="@style/EditText.Profile" style="@style/Profile.EditText"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_link_black_24dp" android:drawableStart="@drawable/ic_link_black_24dp"
android:hint="@string/msg_avatar_url" android:hint="@string/msg_avatar_url"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="6dp">
<include
android:id="@+id/layout_avatar"
layout="@layout/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_member"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="@+id/layout_avatar"
app:layout_constraintLeft_toRightOf="@+id/layout_avatar"
app:layout_constraintTop_toTopOf="@+id/layout_avatar"
tools:text="Ronald Perkins" />
</android.support.constraint.ConstraintLayout>
\ 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"
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_members_list"
android:title="@string/title_members_list"
app:showAsAction="never" />
<item <item
android:id="@+id/action_pinned_messages" android:id="@+id/action_pinned_messages"
android:title="@string/title_pinned_messages" android:title="@string/title_pinned_messages"
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
<string name="title_legal_terms">Termos Legais</string> <string name="title_legal_terms">Termos Legais</string>
<string name="title_chats">Chats</string> <string name="title_chats">Chats</string>
<string name="title_profile">Perfil</string> <string name="title_profile">Perfil</string>
<string name="title_members">Membros (%d)</string>
<string name="title_settings">Configurações</string> <string name="title_settings">Configurações</string>
<string name="title_password">Alterar senha</string> <string name="title_password">Alterar senha</string>
<string name="title_update_profile">Editar perfil</string> <string name="title_update_profile">Editar perfil</string>
...@@ -59,6 +60,8 @@ ...@@ -59,6 +60,8 @@
<string name="msg_content_description_show_attachment_options">Mostrar opções de anexo</string> <string name="msg_content_description_show_attachment_options">Mostrar opções de anexo</string>
<string name="msg_you">Você</string> <string name="msg_you">Você</string>
<string name="msg_unknown">Desconhecido</string> <string name="msg_unknown">Desconhecido</string>
<string name="msg_email_address">Endereço de e-mail</string>
<string name="msg_utc_offset">Deslocamento de UTC</string>
<string name="msg_new_password">Informe a nova senha</string> <string name="msg_new_password">Informe a nova senha</string>
<string name="msg_confirm_password">Confirme a nova senha</string> <string name="msg_confirm_password">Confirme a nova senha</string>
...@@ -89,10 +92,20 @@ ...@@ -89,10 +92,20 @@
<string name="permission_deleting_not_allowed">Remoção não permitida</string> <string name="permission_deleting_not_allowed">Remoção não permitida</string>
<string name="permission_pinning_not_allowed">Fixar não permitido</string> <string name="permission_pinning_not_allowed">Fixar não permitido</string>
<!-- Members List -->
<string name="title_members_list">Lista de Membros</string>
<!-- Pinned Messages --> <!-- Pinned Messages -->
<string name="title_pinned_messages">Mensagens Pinadas</string> <string name="title_pinned_messages">Mensagens Pinadas</string>
<!-- 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
...@@ -25,6 +25,8 @@ ...@@ -25,6 +25,8 @@
<color name="colorDim">#99000000</color> <color name="colorDim">#99000000</color>
<color name="colorBackgroundMemberContainer">#4D000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="red">#FFFF0000</color> <color name="red">#FFFF0000</color>
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
<string name="title_legal_terms">Legal Terms</string> <string name="title_legal_terms">Legal Terms</string>
<string name="title_chats">Chats</string> <string name="title_chats">Chats</string>
<string name="title_profile">Profile</string> <string name="title_profile">Profile</string>
<string name="title_members">Members (%d)</string>
<string name="title_settings">Settings</string> <string name="title_settings">Settings</string>
<string name="title_password">Change Password</string> <string name="title_password">Change Password</string>
<string name="title_update_profile">Update profile</string> <string name="title_update_profile">Update profile</string>
...@@ -61,6 +62,8 @@ ...@@ -61,6 +62,8 @@
<string name="msg_content_description_show_attachment_options">Show attachment options</string> <string name="msg_content_description_show_attachment_options">Show attachment options</string>
<string name="msg_you">You</string> <string name="msg_you">You</string>
<string name="msg_unknown">Unknown</string> <string name="msg_unknown">Unknown</string>
<string name="msg_email_address">E-mail address</string>
<string name="msg_utc_offset">UTC offset</string>
<string name="msg_new_password">Enter New Password</string> <string name="msg_new_password">Enter New Password</string>
<string name="msg_confirm_password">Confirm New Password</string> <string name="msg_confirm_password">Confirm New Password</string>
...@@ -91,10 +94,21 @@ ...@@ -91,10 +94,21 @@
<string name="permission_deleting_not_allowed">Deleting is not allowed</string> <string name="permission_deleting_not_allowed">Deleting is not allowed</string>
<string name="permission_pinning_not_allowed">Pinning is not allowed</string> <string name="permission_pinning_not_allowed">Pinning is not allowed</string>
<!-- Members List -->
<string name="title_members_list">Members List</string>
<!-- Pinned Messages --> <!-- Pinned Messages -->
<string name="title_pinned_messages">Pinned Messages</string> <string name="title_pinned_messages">Pinned Messages</string>
<!-- 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