Unverified Commit 7e81822e authored by Filipe de Lima Brito's avatar Filipe de Lima Brito Committed by GitHub

Merge pull request #1860 from RocketChat/observe-channel-changes

[NEW] Observe channel changes
parents 65ebc5bc a0f84aaa
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.PermissionsInteractor
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 +49,30 @@ 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,
permissionsInteractor: PermissionsInteractor
): RoomUiModelMapper {
return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl, permissionsInteractor)
}
}
......@@ -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
......@@ -74,6 +75,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
......@@ -97,6 +99,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 +122,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 +130,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,16 +140,21 @@ class ChatRoomPresenter @Inject constructor(
chatRoles = emptyList()
} finally {
// User has at least an 'owner' or 'moderator' role.
val userCanMod = isOwnerOrMod()
val chatRoom = dbManager.getRoom(roomId)
val muted = chatRoom?.chatRoom?.muted ?: emptyList()
val canModerate = isOwnerOrMod()
// Can post anyway if has the 'post-readonly' permission on server.
val userCanPost = userCanMod || permissions.canPostToReadOnlyChannels() ||
!muted.contains(currentLoggedUsername)
chatIsBroadcast = chatRoom?.chatRoom?.run {
broadcast
} ?: false
view.onRoomUpdated(userCanPost, chatIsBroadcast, userCanMod)
val room = dbManager.getRoom(roomId)
room?.let {
chatIsBroadcast = it.chatRoom.broadcast ?: false
val roomUiModel = roomMapper.map(it, true)
launchUI(strategy) {
view.onRoomUpdated(roomUiModel = roomUiModel.copy(
broadcast = chatIsBroadcast,
canModerate = canModerate,
writable = roomUiModel.writable || canModerate
))
}
}
loadMessages(roomId, roomType, clearDataSet = true)
chatRoomMessage?.let { messageHelper.messageIdFromPermalink(it) }
?.let { messageId ->
......@@ -157,10 +166,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"
......@@ -987,7 +1012,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(
writable = canPost)
view.onJoined(roomUiModel = roomUiModel)
view.onRoomUpdated(roomUiModel = roomUiModel)
}
} catch (ex: RocketChatException) {
Timber.e(ex)
}
......@@ -1157,6 +1187,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)
}
......@@ -51,6 +51,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
......@@ -401,17 +402,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()
}
......@@ -790,12 +788,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)
}
}
......@@ -828,8 +825,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
private fun setupMessageComposer(canPost: Boolean) {
if (!canPost) {
private fun setupMessageComposer(roomUiModel: RoomUiModel) {
if (isReadOnly || !roomUiModel.writable) {
text_room_is_read_only.isVisible = true
input_container.isVisible = false
text_room_is_read_only.setText(
......@@ -845,6 +842,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
button_join_chat.isVisible = true
button_join_chat.setOnClickListener { presenter.joinChat(chatRoomId) }
} else {
input_container.isVisible = true
text_room_is_read_only.isVisible = false
button_send.isVisible = false
button_show_attachment_options.alpha = 1f
button_show_attachment_options.isVisible = true
......
......@@ -8,9 +8,8 @@ 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.PermissionsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.showLastMessage
import chat.rocket.android.server.domain.useRealName
......@@ -30,7 +29,8 @@ class RoomUiModelMapper(
private val context: Application,
private val settings: PublicSettings,
private val userInteractor: GetCurrentUserInteractor,
private val serverUrl: String
private val serverUrl: String,
private val permissions: PermissionsInteractor
) {
private val nameUnreadColor = ContextCompat.getColor(context, R.color.colorPrimaryText)
private val nameColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
......@@ -97,7 +97,9 @@ class RoomUiModelMapper(
avatar = serverUrl.avatarUrl(name!!, isGroupOrChannel = true),
lastMessage = if(showLastMessage) { mapLastMessage(lastMessage?.sender?.id, lastMessage?.sender?.username,
lastMessage?.sender?.name, lastMessage?.message,
isDirectMessage = type is RoomType.DirectMessage)} else { null }
isDirectMessage = type is RoomType.DirectMessage)} else { null },
muted = muted.orEmpty(),
writable = isChannelWritable(muted)
)
}
}
......@@ -133,11 +135,18 @@ class RoomUiModelMapper(
alert = isUnread,
lastMessage = lastMessageMarkdown,
status = status,
username = if (type is RoomType.DirectMessage) name else null
username = if (type is RoomType.DirectMessage) name else null,
muted = muted.orEmpty(),
writable = isChannelWritable(muted)
)
}
}
private fun isChannelWritable(muted: List<String>?): Boolean {
val canWriteToReadOnlyChannels = permissions.canPostToReadOnlyChannels()
return canWriteToReadOnlyChannels || !muted.orEmpty().contains(currentUser?.username)
}
private fun roomType(type: String): String {
val resources = context.resources
return when (type) {
......@@ -205,4 +214,4 @@ class RoomUiModelMapper(
}
}
}
}
\ No newline at end of file
}
......@@ -15,5 +15,8 @@ data class RoomUiModel(
val lastMessage: CharSequence? = null,
val status: UserStatus? = null,
val username: String? = null,
val broadcast: Boolean = false,
val canModerate: Boolean = false,
val writable: Boolean = true,
val muted: List<String> = emptyList()
)
......@@ -12,6 +12,7 @@ import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.UserDao
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentUserInteractor
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
......@@ -88,9 +89,10 @@ class ChatRoomsFragmentModule {
context: Application,
repository: SettingsRepository,
userInteractor: GetCurrentUserInteractor,
@Named("currentServer") serverUrl: String
@Named("currentServer") serverUrl: String,
permissionsInteractor: PermissionsInteractor
): RoomUiModelMapper {
return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl)
return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl, permissionsInteractor)
}
@Provides
......
......@@ -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) {
......
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