Commit b0d383b1 authored by Leonardo Aramaki's avatar Leonardo Aramaki

Observe channel changes through socket

parent cf91f096
File mode changed from 100644 to 100755
package chat.rocket.android.chatroom.di
import android.app.Application
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.ui.ChatRoomFragment
import chat.rocket.android.chatrooms.adapter.RoomUiModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.db.UserDao
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetCurrentUserInteractor
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
......@@ -42,4 +48,29 @@ class ChatRoomFragmentModule {
@Provides
@PerFragment
fun provideChatRoomDao(manager: DatabaseManager): ChatRoomDao = manager.chatRoomDao()
@Provides
@PerFragment
fun provideUserDao(manager: DatabaseManager): UserDao = manager.userDao()
@Provides
@PerFragment
fun provideGetCurrentUserInteractor(
tokenRepository: TokenRepository,
@Named("currentServer") serverUrl: String,
userDao: UserDao
): GetCurrentUserInteractor {
return GetCurrentUserInteractor(tokenRepository, serverUrl, userDao)
}
@Provides
@PerFragment
fun provideRoomMapper(
context: Application,
repository: SettingsRepository,
userInteractor: GetCurrentUserInteractor,
@Named("currentServer") serverUrl: String
): RoomUiModelMapper {
return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl)
}
}
......@@ -15,6 +15,7 @@ import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.chatrooms.adapter.RoomUiModelMapper
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
......@@ -34,7 +35,6 @@ import chat.rocket.android.server.domain.uploadMimeTypeFilter
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extension.compressImageAndGetByteArray
import chat.rocket.android.util.extension.getByteArray
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.avatarUrl
......@@ -73,6 +73,7 @@ import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.ChatRoomRole
import chat.rocket.core.model.Command
import chat.rocket.core.model.Message
import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.android.UI
......@@ -81,7 +82,6 @@ import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import org.threeten.bp.Instant
import timber.log.Timber
import java.io.InputStream
import java.util.*
import javax.inject.Inject
......@@ -97,6 +97,7 @@ class ChatRoomPresenter @Inject constructor(
private val analyticsManager: AnalyticsManager,
private val userHelper: UserHelper,
private val mapper: UiModelMapper,
private val roomMapper: RoomUiModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor,
private val messageHelper: MessageHelper,
private val dbManager: DatabaseManager,
......@@ -119,6 +120,7 @@ class ChatRoomPresenter @Inject constructor(
private var typingStatusSubscriptionId: String? = null
private var lastState = manager.state
private var typingStatusList = arrayListOf<String>()
private val roomChangesChannel = Channel<Room>(Channel.CONFLATED)
fun setupChatRoom(
roomId: String,
......@@ -126,7 +128,7 @@ class ChatRoomPresenter @Inject constructor(
roomType: String,
chatRoomMessage: String? = null
) {
launchUI(strategy) {
launch(CommonPool + strategy.jobs) {
try {
chatRoles = if (roomTypeOf(roomType) !is RoomType.DirectMessage) {
client.chatRoomRoles(roomType = roomTypeOf(roomType), roomName = roomName)
......@@ -136,13 +138,22 @@ class ChatRoomPresenter @Inject constructor(
chatRoles = emptyList()
} finally {
// User has at least an 'owner' or 'moderator' role.
val userCanMod = isOwnerOrMod()
val canModerate = isOwnerOrMod()
// Can post anyway if has the 'post-readonly' permission on server.
val userCanPost = userCanMod || permissions.canPostToReadOnlyChannels()
chatIsBroadcast = dbManager.getRoom(roomId)?.chatRoom?.run {
broadcast
} ?: false
view.onRoomUpdated(userCanPost, chatIsBroadcast, userCanMod)
val room = dbManager.getRoom(roomId)
val canPost = canModerate || permissions.canPostToReadOnlyChannels()
room?.let {
chatIsBroadcast = it.chatRoom.broadcast ?: false
val roomUiModel = roomMapper.map(it, true)
launchUI(strategy) {
view.onRoomUpdated(roomUiModel = roomUiModel.copy(
broadcast = chatIsBroadcast,
canModerate = canModerate,
canPost = canPost
))
}
}
loadMessages(roomId, roomType, clearDataSet = true)
chatRoomMessage?.let { messageHelper.messageIdFromPermalink(it) }
?.let { messageId ->
......@@ -154,10 +165,26 @@ class ChatRoomPresenter @Inject constructor(
true
)
}
subscribeRoomChanges()
}
}
}
private suspend fun subscribeRoomChanges() {
chatRoomId?.let {
manager.addRoomChannel(it, roomChangesChannel)
for (room in roomChangesChannel) {
dbManager.getRoom(room.id)?.let {
view.onRoomUpdated(roomMapper.map(chatRoom = it, showLastMessage = true))
}
}
}
}
private fun unsubscribeRoomChanges() {
chatRoomId?.let { manager.removeRoomChannel(it) }
}
private fun isOwnerOrMod(): Boolean {
return chatRoles.firstOrNull { it.user.username == currentLoggedUsername }?.roles?.any {
it == "owner" || it == "moderator"
......@@ -980,7 +1007,12 @@ class ChatRoomPresenter @Inject constructor(
try {
retryIO("joinChat($chatRoomId)") { client.joinChat(chatRoomId) }
val canPost = permissions.canPostToReadOnlyChannels()
view.onJoined(canPost)
dbManager.getRoom(chatRoomId)?.let {
val roomUiModel = roomMapper.map(it, true).copy(
canPost = canPost)
view.onJoined(roomUiModel = roomUiModel)
view.onRoomUpdated(roomUiModel = roomUiModel)
}
} catch (ex: RocketChatException) {
Timber.e(ex)
}
......@@ -1150,6 +1182,7 @@ class ChatRoomPresenter @Inject constructor(
}
fun disconnect() {
unsubscribeRoomChanges()
unsubscribeTypingStatus()
if (chatRoomId != null) {
unsubscribeMessages(chatRoomId.toString())
......
......@@ -5,6 +5,7 @@ import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.socket.model.State
......@@ -131,12 +132,7 @@ interface ChatRoomView : LoadingView, MessageView {
fun populateEmojiSuggestions(emojis: List<EmojiSuggestionUiModel>)
/**
* This user has joined the chat callback.
*
* @param userCanPost Whether the user can post a message or not.
*/
fun onJoined(userCanPost: Boolean)
fun onJoined(roomUiModel: RoomUiModel)
fun showReactionsPopup(messageId: String)
......@@ -147,9 +143,6 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun populateCommandSuggestions(commands: List<CommandSuggestionUiModel>)
/**
* Communicate whether it's a broadcast channel and if current user can post to it.
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
fun onRoomUpdated(roomUiModel: RoomUiModel)
}
......@@ -48,6 +48,7 @@ import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.draw.main.ui.DRAWING_BYTE_ARRAY_EXTRA_DATA
import chat.rocket.android.draw.main.ui.DrawingActivity
import chat.rocket.android.emoji.ComposerEditText
......@@ -380,17 +381,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
empty_chat_view.isVisible = adapter.itemCount == 0
}
override fun onRoomUpdated(
userCanPost: Boolean,
channelIsBroadcast: Boolean,
userCanMod: Boolean
) {
override fun onRoomUpdated(roomUiModel: RoomUiModel) {
// TODO: We should rely solely on the user being able to post, but we cannot guarantee
// that the "(channels|groups).roles" endpoint is supported by the server in use.
ui {
setupMessageComposer(userCanPost)
isBroadcastChannel = channelIsBroadcast
if (isBroadcastChannel && !userCanMod) {
setupToolbar(roomUiModel.name.toString())
setupMessageComposer(roomUiModel)
isBroadcastChannel = roomUiModel.broadcast
if (isBroadcastChannel && !roomUiModel.canModerate) {
disableMenu = true
activity?.invalidateOptionsMenu()
}
......@@ -731,12 +729,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun onJoined(userCanPost: Boolean) {
override fun onJoined(roomUiModel: RoomUiModel) {
ui {
input_container.isVisible = true
button_join_chat.isVisible = false
isSubscribed = true
setupMessageComposer(userCanPost)
}
}
......@@ -769,8 +766,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
private fun setupMessageComposer(canPost: Boolean) {
if (isReadOnly && !canPost) {
private fun setupMessageComposer(roomUiModel: RoomUiModel) {
if (isReadOnly && !roomUiModel.canPost) {
text_room_is_read_only.isVisible = true
input_container.isVisible = false
} else if (!isSubscribed && roomTypeOf(chatRoomType) !is RoomType.DirectMessage) {
......
......@@ -8,8 +8,6 @@ import androidx.core.text.color
import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.checkIfMyself
import chat.rocket.android.server.domain.GetCurrentUserInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.showLastMessage
......@@ -102,6 +100,7 @@ class RoomUiModelMapper(
}
}
@Synchronized
fun map(chatRoom: ChatRoom, showLastMessage:Boolean = true): RoomUiModel {
return with(chatRoom.chatRoom) {
val isUnread = alert || unread > 0
......
......@@ -14,5 +14,8 @@ data class RoomUiModel(
val alert: Boolean = false,
val lastMessage: CharSequence? = null,
val status: UserStatus? = null,
val username: String? = null
)
\ No newline at end of file
val username: String? = null,
val broadcast: Boolean = false,
val canModerate: Boolean = false,
val canPost: Boolean = true
)
......@@ -22,6 +22,7 @@ import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself
import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel
......@@ -45,6 +46,7 @@ class ConnectionManager(
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val userDataChannels = ArrayList<Channel<Myself>>()
private val roomsChannels = LinkedHashMap<String, Channel<Room>>()
private val subscriptionIdMap = HashMap<String, String>()
private var subscriptionId: String? = null
......@@ -127,6 +129,18 @@ class ConnectionManager(
maxSize = 10) { batch ->
Timber.d("processing Stream batch: ${batch.size} - $batch")
dbManager.processChatRoomsBatch(batch)
batch.forEach {
//TODO - Do we need to handle Type.Removed and Type.Inserted here?
if (it.type == Type.Updated) {
if (it.data is Room) {
val room = it.data as Room
roomsChannels[it.data.id]?.let { channel ->
channel.offer(room)
}
}
}
}
}
val messagesActor = createBatchActor<Message>(messagesContext, parent = connectJob,
......@@ -241,6 +255,14 @@ class ConnectionManager(
fun removeUserDataChannel(channel: Channel<Myself>) = userDataChannels.remove(channel)
fun addRoomChannel(roomId: String, channel: Channel<Room>) {
roomsChannels[roomId] = channel
}
fun removeRoomChannel(roomId: String) {
roomsChannels.remove(roomId)
}
fun subscribeRoomMessages(roomId: String, channel: Channel<Message>) {
val oldSub = roomMessagesChannels.put(roomId, channel)
if (oldSub != null) {
......
File mode changed from 100644 to 100755
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