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

Merge branch 'develop' into wear-module

parents 2fa3abb1 ca99f416
...@@ -13,7 +13,7 @@ android { ...@@ -13,7 +13,7 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 2020 versionCode 2021
versionName "2.2.0" versionName "2.2.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
......
...@@ -5,7 +5,21 @@ import android.view.MenuItem ...@@ -5,7 +5,21 @@ 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.ui.chatRoomIntent
import chat.rocket.android.chatroom.viewmodel.AudioAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.AuthorAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.BaseFileAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.ColorAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.GenericFileAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.MessageReplyViewModel
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel
import chat.rocket.android.chatroom.viewmodel.VideoAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.toViewType
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.widget.emoji.EmojiReactionListener import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
...@@ -15,6 +29,7 @@ import java.security.InvalidParameterException ...@@ -15,6 +29,7 @@ import java.security.InvalidParameterException
class ChatRoomAdapter( class ChatRoomAdapter(
private val roomType: String, private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?, private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true, private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null private val reactionListener: EmojiReactionListener? = null
...@@ -64,6 +79,12 @@ class ChatRoomAdapter( ...@@ -64,6 +79,12 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_file_attachment) val view = parent.inflate(R.layout.item_file_attachment)
GenericFileAttachmentViewHolder(view, actionsListener, reactionListener) GenericFileAttachmentViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.MESSAGE_REPLY -> {
val view = parent.inflate(R.layout.item_message_reply)
MessageReplyViewHolder(view, actionsListener, reactionListener) { roomName, permalink ->
presenter?.openDirectMessage(roomName, permalink)
}
}
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
} }
...@@ -115,6 +136,8 @@ class ChatRoomAdapter( ...@@ -115,6 +136,8 @@ class ChatRoomAdapter(
holder.bind(dataSet[position] as ColorAttachmentViewModel) holder.bind(dataSet[position] as ColorAttachmentViewModel)
is GenericFileAttachmentViewHolder -> is GenericFileAttachmentViewHolder ->
holder.bind(dataSet[position] as GenericFileAttachmentViewModel) holder.bind(dataSet[position] as GenericFileAttachmentViewModel)
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyViewModel)
} }
} }
...@@ -196,10 +219,10 @@ class ChatRoomAdapter( ...@@ -196,10 +219,10 @@ class ChatRoomAdapter(
message.apply { message.apply {
when (item.itemId) { when (item.itemId) {
R.id.action_message_reply -> { R.id.action_message_reply -> {
presenter?.citeMessage(roomType, id, true) presenter?.citeMessage(roomName, roomType, id, true)
} }
R.id.action_message_quote -> { R.id.action_message_quote -> {
presenter?.citeMessage(roomType, id, false) presenter?.citeMessage(roomName, roomType, id, false)
} }
R.id.action_message_copy -> { R.id.action_message_copy -> {
presenter?.copyMessage(id) presenter?.copyMessage(id)
......
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageReplyViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message_reply.view.*
class MessageReplyViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
private val replyCallback: (roomName: String, permalink: String) -> Unit
) : BaseViewHolder<MessageReplyViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(itemView)
}
}
override fun bindViews(data: MessageReplyViewModel) {
with(itemView) {
button_message_reply.setOnClickListener {
with(data.rawData) {
replyCallback.invoke(roomName, permalink)
}
}
}
}
}
\ No newline at end of file
...@@ -37,7 +37,7 @@ class MessageViewHolder( ...@@ -37,7 +37,7 @@ class MessageViewHolder(
if (data.isTemporary) Color.GRAY else Color.BLACK if (data.isTemporary) Color.GRAY else Color.BLACK
) )
data.message.let { data.message.let {
text_edit_indicator.isVisible = it.isSystemMessage() && it.editedBy != null text_edit_indicator.isVisible = !it.isSystemMessage() && it.editedBy != null
image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false
} }
} }
......
package chat.rocket.android.chatroom.domain
data class MessageReply(
val roomName: String,
val permalink: String
)
\ No newline at end of file
...@@ -2,6 +2,7 @@ package chat.rocket.android.chatroom.presentation ...@@ -2,6 +2,7 @@ package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.members.ui.newInstance import chat.rocket.android.members.ui.newInstance
import chat.rocket.android.server.ui.changeServerIntent import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack import chat.rocket.android.util.extensions.addFragmentBackStack
...@@ -30,4 +31,17 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) { ...@@ -30,4 +31,17 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
activity.startActivity(activity.changeServerIntent()) activity.startActivity(activity.changeServerIntent())
activity.finish() activity.finish()
} }
fun toDirectMessage(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean,
isChatRoomCreator: Boolean,
chatRoomMessage: String) {
activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
} }
\ No newline at end of file
...@@ -6,16 +6,28 @@ import chat.rocket.android.chatroom.adapter.AutoCompleteType ...@@ -6,16 +6,28 @@ import chat.rocket.android.chatroom.adapter.AutoCompleteType
import chat.rocket.android.chatroom.adapter.PEOPLE import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.ROOMS import chat.rocket.android.chatroom.adapter.ROOMS
import chat.rocket.android.chatroom.domain.UriInteractor import chat.rocket.android.chatroom.domain.UriInteractor
import chat.rocket.android.chatroom.viewmodel.RoomViewModel
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.chatroom.viewmodel.suggestion.ChatRoomSuggestionViewModel import chat.rocket.android.chatroom.viewmodel.suggestion.ChatRoomSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewModel import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.behaviours.showMessage import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.MessageHelper
import chat.rocket.android.helper.UserHelper import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.username import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.JobSchedulerInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.domain.uploadMaxFileSize
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.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
...@@ -31,7 +43,26 @@ import chat.rocket.core.internal.realtime.setTypingStatus ...@@ -31,7 +43,26 @@ import chat.rocket.core.internal.realtime.setTypingStatus
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.realtime.subscribeTypingStatus import chat.rocket.core.internal.realtime.subscribeTypingStatus
import chat.rocket.core.internal.realtime.unsubscribe import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.rest.* import chat.rocket.core.internal.rest.chatRoomRoles
import chat.rocket.core.internal.rest.commands
import chat.rocket.core.internal.rest.deleteMessage
import chat.rocket.core.internal.rest.getMembers
import chat.rocket.core.internal.rest.history
import chat.rocket.core.internal.rest.joinChat
import chat.rocket.core.internal.rest.markAsRead
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.messages
import chat.rocket.core.internal.rest.pinMessage
import chat.rocket.core.internal.rest.runCommand
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.internal.rest.starMessage
import chat.rocket.core.internal.rest.toggleReaction
import chat.rocket.core.internal.rest.unpinMessage
import chat.rocket.core.internal.rest.unstarMessage
import chat.rocket.core.internal.rest.updateMessage
import chat.rocket.core.internal.rest.uploadFile
import chat.rocket.core.model.ChatRoomRole
import chat.rocket.core.model.Command import chat.rocket.core.model.Command
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself import chat.rocket.core.model.Myself
...@@ -49,7 +80,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -49,7 +80,7 @@ class ChatRoomPresenter @Inject constructor(
private val view: ChatRoomView, private val view: ChatRoomView,
private val navigator: ChatRoomNavigator, private val navigator: ChatRoomNavigator,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val getChatRoomsInteractor: GetChatRoomsInteractor, private val chatRoomsInteractor: ChatRoomsInteractor,
private val permissions: PermissionsInteractor, private val permissions: PermissionsInteractor,
private val uriInteractor: UriInteractor, private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository, private val messagesRepository: MessagesRepository,
...@@ -59,6 +90,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -59,6 +90,7 @@ class ChatRoomPresenter @Inject constructor(
private val userHelper: UserHelper, private val userHelper: UserHelper,
private val mapper: ViewModelMapper, private val mapper: ViewModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor, private val jobSchedulerInteractor: JobSchedulerInteractor,
private val messageHelper: MessageHelper,
getSettingsInteractor: GetSettingsInteractor, getSettingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor, serverInteractor: GetCurrentServerInteractor,
factory: ConnectionManagerFactory factory: ConnectionManagerFactory
...@@ -67,23 +99,50 @@ class ChatRoomPresenter @Inject constructor( ...@@ -67,23 +99,50 @@ class ChatRoomPresenter @Inject constructor(
private val manager = factory.create(currentServer) private val manager = factory.create(currentServer)
private val client = manager.client private val client = manager.client
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!) private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
private val currentLoggedUsername = localRepository.username() private val currentLoggedUsername = userHelper.username()
private val messagesChannel = Channel<Message>() private val messagesChannel = Channel<Message>()
private var chatRoomId: String? = null private var chatRoomId: String? = null
private var chatRoomType: String? = null private var chatRoomType: String? = null
private var chatIsBroadcast: Boolean = false
private var chatRoles = emptyList<ChatRoomRole>()
private val stateChannel = Channel<State>() private val stateChannel = Channel<State>()
private var typingStatusSubscriptionId: String? = null private var typingStatusSubscriptionId: String? = null
private var lastState = manager.state private var lastState = manager.state
private var typingStatusList = arrayListOf<String>() private var typingStatusList = arrayListOf<String>()
fun setupChatRoom() { fun setupChatRoom(roomId: String, roomName: String, roomType: String, chatRoomMessage: String? = null) {
launchUI(strategy) { launchUI(strategy) {
val canPost = permissions.canPostToReadOnlyChannels() try {
view.onRoomChanged(canPost) chatRoles = if (roomTypeOf(roomType) !is RoomType.DirectMessage) {
client.chatRoomRoles(roomType = roomTypeOf(roomType), roomName = roomName)
} else emptyList()
} catch (ex: RocketChatException) {
Timber.e(ex)
chatRoles = emptyList()
} finally {
// User has at least an 'owner' or 'moderator' role.
val userCanMod = isOwnerOrMod()
// Can post anyway if has the 'post-readonly' permission on server.
val userCanPost = userCanMod || permissions.canPostToReadOnlyChannels()
chatIsBroadcast = chatRoomsInteractor.getById(currentServer, roomId)?.run {
broadcast
} ?: false
view.onRoomUpdated(userCanPost, chatIsBroadcast, userCanMod)
loadMessages(roomId, roomType)
chatRoomMessage?.let { messageHelper.messageIdFromPermalink(it) }?.let { messageId ->
val name = messageHelper.roomNameFromPermalink(chatRoomMessage)
citeMessage(name!!, messageHelper.roomTypeFromPermalink(chatRoomMessage)!!, messageId, true)
}
} }
} }
}
private fun isOwnerOrMod(): Boolean {
return chatRoles.firstOrNull { it.user.username == currentLoggedUsername }?.roles?.any {
it == "owner" || it == "moderator"
} ?: false
}
fun loadMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) { fun loadMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
this.chatRoomId = chatRoomId this.chatRoomId = chatRoomId
...@@ -93,7 +152,8 @@ class ChatRoomPresenter @Inject constructor( ...@@ -93,7 +152,8 @@ class ChatRoomPresenter @Inject constructor(
try { try {
if (offset == 0L) { if (offset == 0L) {
val localMessages = messagesRepository.getByRoomId(chatRoomId) val localMessages = messagesRepository.getByRoomId(chatRoomId)
val oldMessages = mapper.map(localMessages) val oldMessages = mapper.map(localMessages, RoomViewModel(roles = chatRoles,
isBroadcast = chatIsBroadcast, isRoom = true))
if (oldMessages.isNotEmpty()) { if (oldMessages.isNotEmpty()) {
view.showMessages(oldMessages) view.showMessages(oldMessages)
loadMissingMessages() loadMissingMessages()
...@@ -133,8 +193,8 @@ class ChatRoomPresenter @Inject constructor( ...@@ -133,8 +193,8 @@ class ChatRoomPresenter @Inject constructor(
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
} }
messagesRepository.saveAll(messages) messagesRepository.saveAll(messages)
val allMessages = mapper.map(messages) view.showMessages(mapper.map(messages, RoomViewModel(roles = chatRoles,
view.showMessages(allMessages) isBroadcast = chatIsBroadcast, isRoom = true)))
} }
fun sendMessage(chatRoomId: String, text: String, messageId: String?) { fun sendMessage(chatRoomId: String, text: String, messageId: String?) {
...@@ -168,9 +228,10 @@ class ChatRoomPresenter @Inject constructor( ...@@ -168,9 +228,10 @@ class ChatRoomPresenter @Inject constructor(
isTemporary = true isTemporary = true
) )
try { try {
val message = client.sendMessage(id, chatRoomId, text)
messagesRepository.save(newMessage) messagesRepository.save(newMessage)
view.showNewMessage(mapper.map(newMessage)) val message = client.sendMessage(id, chatRoomId, text)
view.showNewMessage(mapper.map(newMessage, RoomViewModel(
roles = chatRoles, isBroadcast = chatIsBroadcast)))
message message
} catch (ex: Exception) { } catch (ex: Exception) {
// Ok, not very beautiful, but the backend sends us a not valid response // Ok, not very beautiful, but the backend sends us a not valid response
...@@ -311,7 +372,8 @@ class ChatRoomPresenter @Inject constructor( ...@@ -311,7 +372,8 @@ class ChatRoomPresenter @Inject constructor(
Timber.d("History: $messages") Timber.d("History: $messages")
if (messages.result.isNotEmpty()) { if (messages.result.isNotEmpty()) {
val models = mapper.map(messages.result) val models = mapper.map(messages.result, RoomViewModel(
roles = chatRoles, isBroadcast = chatIsBroadcast, isRoom = true))
messagesRepository.saveAll(messages.result) messagesRepository.saveAll(messages.result)
launchUI(strategy) { launchUI(strategy) {
...@@ -368,7 +430,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -368,7 +430,7 @@ class ChatRoomPresenter @Inject constructor(
* @param messageId The id of the message to make citation for. * @param messageId The id of the message to make citation for.
* @param mentionAuthor true means the citation is a reply otherwise it's a quote. * @param mentionAuthor true means the citation is a reply otherwise it's a quote.
*/ */
fun citeMessage(roomType: String, messageId: String, mentionAuthor: Boolean) { fun citeMessage(roomName: String, roomType: String, messageId: String, mentionAuthor: Boolean) {
launchUI(strategy) { launchUI(strategy) {
val message = messagesRepository.getById(messageId) val message = messagesRepository.getById(messageId)
val me: Myself? = try { val me: Myself? = try {
...@@ -381,11 +443,19 @@ class ChatRoomPresenter @Inject constructor( ...@@ -381,11 +443,19 @@ class ChatRoomPresenter @Inject constructor(
val id = msg.id val id = msg.id
val username = msg.sender?.username ?: "" val username = msg.sender?.username ?: ""
val mention = if (mentionAuthor && me?.username != username) "@$username" else "" val mention = if (mentionAuthor && me?.username != username) "@$username" else ""
val room = if (roomTypeOf(roomType) is RoomType.DirectMessage) username else roomType val room = if (roomTypeOf(roomType) is RoomType.DirectMessage) username else roomName
val chatRoomType = when (roomTypeOf(roomType)) {
is RoomType.DirectMessage -> "direct"
is RoomType.PrivateGroup -> "group"
is RoomType.Channel -> "channel"
is RoomType.Livechat -> "livechat"
else -> "custom"
}
view.showReplyingAction( view.showReplyingAction(
username = getDisplayName(msg.sender), username = getDisplayName(msg.sender),
replyMarkdown = "[ ]($currentServer/$roomType/$room?msg=$id) $mention ", replyMarkdown = "[ ]($currentServer/$chatRoomType/$room?msg=$id) $mention ",
quotedMessage = mapper.map(message).last().preview?.message ?: "" quotedMessage = mapper.map(message, RoomViewModel(roles = chatRoles,
isBroadcast = chatIsBroadcast)).last().preview?.message ?: ""
) )
} }
} }
...@@ -580,7 +650,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -580,7 +650,7 @@ class ChatRoomPresenter @Inject constructor(
fun loadChatRooms() { fun loadChatRooms() {
launchUI(strategy) { launchUI(strategy) {
try { try {
val chatRooms = getChatRoomsInteractor.getAll(currentServer) val chatRooms = chatRoomsInteractor.getAll(currentServer)
.filterNot { .filterNot {
it.type is RoomType.DirectMessage || it.type is RoomType.Livechat it.type is RoomType.DirectMessage || it.type is RoomType.Livechat
} }
...@@ -613,6 +683,32 @@ class ChatRoomPresenter @Inject constructor( ...@@ -613,6 +683,32 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
fun openDirectMessage(roomName: String, message: String) {
launchUI(strategy) {
try {
chatRoomsInteractor.getByName(currentServer, roomName)?.let {
if (it.type is RoomType.DirectMessage) {
navigator.toDirectMessage(
chatRoomId = it.id,
chatRoomType = it.type.toString(),
chatRoomLastSeen = it.lastSeen ?: -1,
chatRoomName = roomName,
isChatRoomCreator = false,
isChatRoomReadOnly = false,
isChatRoomSubscribed = it.open,
chatRoomMessage = message
)
} else {
throw IllegalStateException("Not a direct-message")
}
}
} catch (ex: Exception) {
Timber.e(ex)
view.showMessage(ex.message!!)
}
}
}
/** /**
* Send an emoji reaction to a message. * Send an emoji reaction to a message.
*/ */
...@@ -736,7 +832,8 @@ class ChatRoomPresenter @Inject constructor( ...@@ -736,7 +832,8 @@ class ChatRoomPresenter @Inject constructor(
private fun updateMessage(streamedMessage: Message) { private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) { launchUI(strategy) {
val viewModelStreamedMessage = mapper.map(streamedMessage) val viewModelStreamedMessage = mapper.map(streamedMessage, RoomViewModel(
roles = chatRoles, isBroadcast = chatIsBroadcast))
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId) val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id } val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index > -1) { if (index > -1) {
......
...@@ -8,6 +8,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewMod ...@@ -8,6 +8,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewMod
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.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
interface ChatRoomView : LoadingView, MessageView { interface ChatRoomView : LoadingView, MessageView {
...@@ -122,9 +123,9 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -122,9 +123,9 @@ interface ChatRoomView : LoadingView, MessageView {
/** /**
* This user has joined the chat callback. * This user has joined the chat callback.
* *
* @param canPost Whether the user can post a message or not. * @param userCanPost Whether the user can post a message or not.
*/ */
fun onJoined(canPost: Boolean) fun onJoined(userCanPost: Boolean)
fun showReactionsPopup(messageId: String) fun showReactionsPopup(messageId: String)
...@@ -135,5 +136,14 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -135,5 +136,14 @@ interface ChatRoomView : LoadingView, MessageView {
*/ */
fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>) fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>)
fun onRoomChanged(canPost: Boolean) /**
* Communicate whether it's a broadcast channel and if current user can post to it.
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
/**
* Open a DM with the user in the given [chatRoom] and pass the [permalink] for the message
* to reply.
*/
fun openDirectMessage(chatRoom: ChatRoom, permalink: String)
} }
\ No newline at end of file
...@@ -60,8 +60,16 @@ class MessageService : JobService() { ...@@ -60,8 +60,16 @@ class MessageService : JobService() {
) )
messageRepository.save(message.copy(isTemporary = false)) messageRepository.save(message.copy(isTemporary = false))
Timber.d("Sent scheduled message given by id: ${message.id}") Timber.d("Sent scheduled message given by id: ${message.id}")
} catch (ex: RocketChatException) { } catch (ex: Exception) {
Timber.e(ex) Timber.e(ex)
// TODO - remove the generic message when we implement :userId:/message subscription
if (ex is IllegalStateException) {
Timber.d(ex, "Probably a read-only problem...")
// TODO: For now we are only going to reschedule when api is fixed.
messageRepository.removeById(message.id)
jobFinished(params, false)
} else {
// some other error
if (ex.message?.contains("E11000", true) == true) { if (ex.message?.contains("E11000", true) == true) {
// XXX: Temporary solution. We need proper error codes from the api. // XXX: Temporary solution. We need proper error codes from the api.
messageRepository.save(message.copy(isTemporary = false)) messageRepository.save(message.copy(isTemporary = false))
...@@ -71,6 +79,7 @@ class MessageService : JobService() { ...@@ -71,6 +79,7 @@ class MessageService : JobService() {
} }
} }
} }
}
companion object { companion object {
const val RETRY_SEND_MESSAGE_ID = 1 const val RETRY_SEND_MESSAGE_ID = 1
......
...@@ -10,11 +10,14 @@ import chat.rocket.android.chatroom.presentation.ChatRoomNavigator ...@@ -10,11 +10,14 @@ import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
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.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.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
import dagger.android.support.HasSupportFragmentInjector import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar.* import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject import javax.inject.Inject
fun Context.chatRoomIntent( fun Context.chatRoomIntent(
...@@ -24,7 +27,8 @@ fun Context.chatRoomIntent( ...@@ -24,7 +27,8 @@ fun Context.chatRoomIntent(
isChatRoomReadOnly: Boolean, isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long, chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean = true, isChatRoomSubscribed: Boolean = true,
isChatRoomOwner: Boolean = false isChatRoomCreator: Boolean = false,
chatRoomMessage: String? = null
): Intent { ): Intent {
return Intent(this, ChatRoomActivity::class.java).apply { return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId) putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
...@@ -33,7 +37,8 @@ fun Context.chatRoomIntent( ...@@ -33,7 +37,8 @@ fun Context.chatRoomIntent(
putExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, isChatRoomReadOnly) putExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen) putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putExtra(INTENT_CHAT_IS_SUBSCRIBED, isChatRoomSubscribed) putExtra(INTENT_CHAT_IS_SUBSCRIBED, isChatRoomSubscribed)
putExtra(INTENT_CHAT_ROOM_IS_OWNER, isChatRoomOwner) putExtra(INTENT_CHAT_ROOM_IS_CREATOR, isChatRoomCreator)
putExtra(INTENT_CHAT_ROOM_MESSAGE, chatRoomMessage)
} }
} }
...@@ -41,9 +46,10 @@ private const val INTENT_CHAT_ROOM_ID = "chat_room_id" ...@@ -41,9 +46,10 @@ private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name" private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type" private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
private const val INTENT_CHAT_ROOM_IS_READ_ONLY = "chat_room_is_read_only" private const val INTENT_CHAT_ROOM_IS_READ_ONLY = "chat_room_is_read_only"
private const val INTENT_CHAT_ROOM_IS_OWNER = "chat_room_is_owner" private const val INTENT_CHAT_ROOM_IS_CREATOR = "chat_room_is_creator"
private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen" private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val INTENT_CHAT_IS_SUBSCRIBED = "is_chat_room_subscribed" private const val INTENT_CHAT_IS_SUBSCRIBED = "is_chat_room_subscribed"
private const val INTENT_CHAT_ROOM_MESSAGE = "chat_room_message"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject @Inject
...@@ -62,7 +68,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -62,7 +68,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false private var isChatRoomReadOnly: Boolean = false
private var isChatRoomSubscribed: Boolean = true private var isChatRoomSubscribed: Boolean = true
private var isChatRoomOwner: Boolean = false private var isChatRoomCreator: Boolean = false
private var chatRoomLastSeen: Long = -1L private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
...@@ -91,8 +97,10 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -91,8 +97,10 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true) isChatRoomReadOnly = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true)
requireNotNull(isChatRoomReadOnly) { "no chat_room_is_read_only provided in Intent extras" } requireNotNull(isChatRoomReadOnly) { "no chat_room_is_read_only provided in Intent extras" }
isChatRoomOwner = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_OWNER, false) isChatRoomCreator = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_CREATOR, false)
requireNotNull(isChatRoomOwner) { "no chat_room_is_owner provided in Intent extras" } requireNotNull(isChatRoomCreator) { "no chat_room_is_creator provided in Intent extras" }
val chatRoomMessage = intent.getStringExtra(INTENT_CHAT_ROOM_MESSAGE)
setupToolbar() setupToolbar()
...@@ -102,10 +110,8 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -102,10 +110,8 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
if (supportFragmentManager.findFragmentByTag(TAG_CHAT_ROOM_FRAGMENT) == null) { if (supportFragmentManager.findFragmentByTag(TAG_CHAT_ROOM_FRAGMENT) == null) {
addFragment(TAG_CHAT_ROOM_FRAGMENT, R.id.fragment_container) { addFragment(TAG_CHAT_ROOM_FRAGMENT, R.id.fragment_container) {
newInstance( newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage)
isChatRoomSubscribed, isChatRoomOwner
)
} }
} }
} }
...@@ -120,13 +126,45 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -120,13 +126,45 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
private fun setupToolbar() { private fun setupToolbar() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp) toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
toolbar.title = chatRoomName text_room_name.textContent = chatRoomName
showRoomTypeIcon(true)
toolbar.setNavigationOnClickListener { finishActivity() } toolbar.setNavigationOnClickListener { finishActivity() }
} }
fun showRoomTypeIcon(showRoomTypeIcon: Boolean) {
if (showRoomTypeIcon) {
val roomType = roomTypeOf(chatRoomType)
val drawable = when (roomType) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, this)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, this)
}
is RoomType.DirectMessage -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, this)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.white)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
}
} else {
text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
}
fun setupToolbarTitle(toolbarTitle: String) { fun setupToolbarTitle(toolbarTitle: String) {
supportActionBar?.title = toolbarTitle text_room_name.textContent = toolbarTitle
} }
private fun finishActivity() { private fun finishActivity() {
......
...@@ -13,12 +13,22 @@ import android.support.v4.app.Fragment ...@@ -13,12 +13,22 @@ import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.view.*
import androidx.core.text.bold import androidx.core.text.bold
import androidx.core.view.isVisible import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.* import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.adapter.CommandSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.BaseViewModel import chat.rocket.android.chatroom.viewmodel.BaseViewModel
...@@ -29,9 +39,28 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewMod ...@@ -29,9 +39,28 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewMod
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.asObservable
import chat.rocket.android.widget.emoji.* import chat.rocket.android.util.extensions.circularRevealOrUnreveal
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.hideKeyboard
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.isAtBottom
import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.widget.emoji.ComposerEditText
import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiKeyboardListener
import chat.rocket.android.widget.emoji.EmojiKeyboardPopup
import chat.rocket.android.widget.emoji.EmojiListenerAdapter
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiPickerPopup
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
...@@ -51,7 +80,8 @@ fun newInstance( ...@@ -51,7 +80,8 @@ fun newInstance(
isChatRoomReadOnly: Boolean, isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long, chatRoomLastSeen: Long,
isSubscribed: Boolean = true, isSubscribed: Boolean = true,
isChatRoomOwner: Boolean = false isChatRoomCreator: Boolean = false,
chatRoomMessage: String? = null
): Fragment { ): Fragment {
return ChatRoomFragment().apply { return ChatRoomFragment().apply {
arguments = Bundle(1).apply { arguments = Bundle(1).apply {
...@@ -61,7 +91,8 @@ fun newInstance( ...@@ -61,7 +91,8 @@ fun newInstance(
putBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly) putBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putLong(BUNDLE_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen) putLong(BUNDLE_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED, isSubscribed) putBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED, isSubscribed)
putBoolean(BUNDLE_CHAT_ROOM_IS_OWNER, isChatRoomOwner) putBoolean(BUNDLE_CHAT_ROOM_IS_CREATOR, isChatRoomCreator)
putString(BUNDLE_CHAT_ROOM_MESSAGE, chatRoomMessage)
} }
} }
} }
...@@ -73,9 +104,11 @@ private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only" ...@@ -73,9 +104,11 @@ private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val REQUEST_CODE_FOR_PERFORM_SAF = 42 private const val REQUEST_CODE_FOR_PERFORM_SAF = 42
private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen" private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val BUNDLE_CHAT_ROOM_IS_SUBSCRIBED = "chat_room_is_subscribed" private const val BUNDLE_CHAT_ROOM_IS_SUBSCRIBED = "chat_room_is_subscribed"
private const val BUNDLE_CHAT_ROOM_IS_OWNER = "chat_room_is_owner" private const val BUNDLE_CHAT_ROOM_IS_CREATOR = "chat_room_is_creator"
private const val BUNDLE_CHAT_ROOM_MESSAGE = "chat_room_message"
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener { class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
@Inject @Inject
lateinit var presenter: ChatRoomPresenter lateinit var presenter: ChatRoomPresenter
@Inject @Inject
...@@ -84,9 +117,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -84,9 +117,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
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
private var chatRoomMessage: String? = null
private var isSubscribed: Boolean = true private var isSubscribed: Boolean = true
private var isChatRoomReadOnly: Boolean = false private var isChatRoomReadOnly: Boolean = false
private var isChatRoomOwner: Boolean = false private var isChatRoomCreator: Boolean = false
private var isBroadcastChannel: Boolean = false
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var chatRoomLastSeen: Long = -1 private var chatRoomLastSeen: Long = -1
private lateinit var actionSnackbar: ActionSnackbar private lateinit var actionSnackbar: ActionSnackbar
...@@ -127,8 +162,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -127,8 +162,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
isChatRoomReadOnly = bundle.getBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY) isChatRoomReadOnly = bundle.getBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY)
isSubscribed = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED) isSubscribed = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED)
chatRoomLastSeen = bundle.getLong(BUNDLE_CHAT_ROOM_LAST_SEEN) chatRoomLastSeen = bundle.getLong(BUNDLE_CHAT_ROOM_LAST_SEEN)
isChatRoomOwner = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_OWNER) isChatRoomCreator = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_CREATOR)
chatRoomMessage = bundle.getString(BUNDLE_CHAT_ROOM_MESSAGE)
} else { } else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" } requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
} }
...@@ -146,13 +181,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -146,13 +181,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupToolbar(chatRoomName) setupToolbar(chatRoomName)
presenter.setupChatRoom() presenter.setupChatRoom(chatRoomId, chatRoomName, chatRoomType, chatRoomMessage)
presenter.loadMessages(chatRoomId, chatRoomType)
presenter.loadChatRooms() presenter.loadChatRooms()
setupRecyclerView() setupRecyclerView()
setupFab() setupFab()
setupSuggestionsView() setupSuggestionsView()
setupActionSnackbar() setupActionSnackbar()
activity?.apply {
(this as? ChatRoomActivity)?.showRoomTypeIcon(true)
}
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
...@@ -187,6 +224,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -187,6 +224,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
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.chatroom_actions, menu) inflater.inflate(R.menu.chatroom_actions, menu)
menu.findItem(R.id.action_members_list)?.isVisible = !isBroadcastChannel
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
...@@ -229,7 +267,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -229,7 +267,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (recycler_view.adapter == null) { if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter( adapter = ChatRoomAdapter(
chatRoomType, presenter, chatRoomType, chatRoomName, presenter,
reactionListener = this@ChatRoomFragment reactionListener = this@ChatRoomFragment
) )
recycler_view.adapter = adapter recycler_view.adapter = adapter
...@@ -251,8 +289,16 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -251,8 +289,16 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun onRoomChanged(canPost: Boolean) { override fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean) {
setupMessageComposer(isChatRoomOwner || canPost) // 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.
setupMessageComposer(userCanPost)
isBroadcastChannel = channelIsBroadcast
if (isBroadcastChannel && !userCanMod) activity?.invalidateOptionsMenu()
}
override fun openDirectMessage(chatRoom: ChatRoom, permalink: String) {
} }
private fun toggleNoChatView(size: Int) { private fun toggleNoChatView(size: Int) {
...@@ -316,7 +362,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -316,7 +362,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (!recyclerView.canScrollVertically(1)) { if (!recyclerView.canScrollVertically(1)) {
button_fab.hide() button_fab.hide()
} else { } else {
if (dy < 0 && !button_fab.isVisible()) { if (dy < 0 && !button_fab.isVisible) {
button_fab.show() button_fab.show()
} }
} }
...@@ -434,12 +480,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -434,12 +480,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
actionSnackbar.text = quotedMessage actionSnackbar.text = quotedMessage
actionSnackbar.show() actionSnackbar.show()
KeyboardHelper.showSoftKeyboard(text_message) KeyboardHelper.showSoftKeyboard(text_message)
if (!recycler_view.isAtBottom()) {
if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
}
}
} }
} }
...@@ -586,12 +626,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -586,12 +626,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun onJoined(canPost: Boolean) { override fun onJoined(userCanPost: Boolean) {
ui { ui {
input_container.setVisible(true) input_container.setVisible(true)
button_join_chat.setVisible(false) button_join_chat.setVisible(false)
isSubscribed = true isSubscribed = true
setupMessageComposer(isChatRoomOwner) setupMessageComposer(userCanPost)
} }
} }
...@@ -623,7 +663,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -623,7 +663,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
private fun setupMessageComposer(canPost: Boolean) { private fun setupMessageComposer(canPost: Boolean) {
if (!canPost && isChatRoomReadOnly) { if (isChatRoomReadOnly && !canPost) {
text_room_is_read_only.setVisible(true) text_room_is_read_only.setVisible(true)
input_container.setVisible(false) input_container.setVisible(false)
} else if (!isSubscribed) { } else if (!isSubscribed) {
...@@ -728,7 +768,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -728,7 +768,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
setReactionButtonIcon(R.drawable.ic_keyboard_black_24dp) setReactionButtonIcon(R.drawable.ic_keyboard_black_24dp)
} else { } else {
// If popup is showing, simply dismiss it to show the undelying text keyboard // If popup is showing, simply dismiss it to show the underlying text keyboard
emojiKeyboardPopup.dismiss() emojiKeyboardPopup.dismiss()
setReactionButtonIcon(R.drawable.ic_reaction_24dp) setReactionButtonIcon(R.drawable.ic_reaction_24dp)
} }
......
...@@ -24,7 +24,8 @@ interface BaseViewModel<out T> { ...@@ -24,7 +24,8 @@ interface BaseViewModel<out T> {
MESSAGE_ATTACHMENT(6), MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7), AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8), COLOR_ATTACHMENT(8),
GENERIC_FILE_ATTACHMENT(9) GENERIC_FILE_ATTACHMENT(9),
MESSAGE_REPLY(10)
} }
} }
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.android.chatroom.domain.MessageReply
import chat.rocket.core.model.Message
data class MessageReplyViewModel(
override val rawData: MessageReply,
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>?,
override var preview: Message?,
override var isTemporary: Boolean = false,
override val message: Message
) : BaseViewModel<MessageReply> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_REPLY.viewType
override val layoutId: Int
get() = R.layout.item_message_reply
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.core.model.ChatRoomRole
data class RoomViewModel(
val roles: List<ChatRoomRole>,
val isBroadcast: Boolean = false,
val isRoom: Boolean = false
)
\ No newline at end of file
...@@ -13,16 +13,32 @@ import androidx.core.text.buildSpannedString ...@@ -13,16 +13,32 @@ import androidx.core.text.buildSpannedString
import androidx.core.text.color import androidx.core.text.color
import androidx.core.text.scale import androidx.core.text.scale
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.domain.MessageReply
import chat.rocket.android.helper.MessageHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.baseUrl
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.isNotNullNorEmpty import chat.rocket.android.util.extensions.isNotNullNorEmpty
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Message 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.Attachment
import chat.rocket.core.model.attachment.AudioAttachment
import chat.rocket.core.model.attachment.AuthorAttachment
import chat.rocket.core.model.attachment.ColorAttachment
import chat.rocket.core.model.attachment.FileAttachment
import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.MessageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.isSystemMessage 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
...@@ -34,6 +50,8 @@ import javax.inject.Inject ...@@ -34,6 +50,8 @@ import javax.inject.Inject
class ViewModelMapper @Inject constructor( class ViewModelMapper @Inject constructor(
private val context: Context, private val context: Context,
private val parser: MessageParser, private val parser: MessageParser,
private val roomsInteractor: ChatRoomsInteractor,
private val messageHelper: MessageHelper,
tokenRepository: TokenRepository, tokenRepository: TokenRepository,
serverInteractor: GetCurrentServerInteractor, serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor, getSettingsInteractor: GetSettingsInteractor,
...@@ -47,21 +65,24 @@ class ViewModelMapper @Inject constructor( ...@@ -47,21 +65,24 @@ class ViewModelMapper @Inject constructor(
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY) private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
private val secondaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText) private val secondaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
suspend fun map(message: Message): List<BaseViewModel<*>> { suspend fun map(message: Message, roomViewModel: RoomViewModel = RoomViewModel(
return translate(message) roles = emptyList(), isBroadcast = true)): List<BaseViewModel<*>> {
return translate(message, roomViewModel)
} }
suspend fun map(messages: List<Message>): List<BaseViewModel<*>> = withContext(CommonPool) { suspend fun map(messages: List<Message>, roomViewModel: RoomViewModel = RoomViewModel(
roles = emptyList(), isBroadcast = true)): List<BaseViewModel<*>> = withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>(messages.size) val list = ArrayList<BaseViewModel<*>>(messages.size)
messages.forEach { messages.forEach {
list.addAll(translate(it)) list.addAll(translate(it, roomViewModel))
} }
return@withContext list return@withContext list
} }
private suspend fun translate(message: Message): List<BaseViewModel<*>> = withContext(CommonPool) { private suspend fun translate(message: Message, roomViewModel: RoomViewModel)
: List<BaseViewModel<*>> = withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>() val list = ArrayList<BaseViewModel<*>>()
message.urls?.forEach { message.urls?.forEach {
...@@ -86,9 +107,40 @@ class ViewModelMapper @Inject constructor( ...@@ -86,9 +107,40 @@ class ViewModelMapper @Inject constructor(
list[i].nextDownStreamMessage = next list[i].nextDownStreamMessage = next
} }
if (isBroadcastReplyAvailable(roomViewModel, message)) {
roomsInteractor.getById(currentServer, message.roomId)?.let { chatRoom ->
val replyViewModel = mapMessageReply(message, chatRoom)
list.first().nextDownStreamMessage = replyViewModel
list.add(0, replyViewModel)
}
}
return@withContext list return@withContext list
} }
private fun isBroadcastReplyAvailable(roomViewModel: RoomViewModel, message: Message): Boolean {
val senderUsername = message.sender?.username
return roomViewModel.isRoom && roomViewModel.isBroadcast &&
!message.isSystemMessage() &&
senderUsername != currentUsername
}
private fun mapMessageReply(message: Message, chatRoom: ChatRoom): MessageReplyViewModel {
val name = message.sender?.name
val roomName = if (settings.useRealName() && name != null) name else message.sender?.username
?: ""
val permalink = messageHelper.createPermalink(message, chatRoom)
return MessageReplyViewModel(
messageId = message.id,
isTemporary = false,
reactions = emptyList(),
message = message,
preview = mapMessagePreview(message),
rawData = MessageReply(roomName = roomName, permalink = permalink),
nextDownStreamMessage = null
)
}
private fun mapUrl(message: Message, url: Url): BaseViewModel<*>? { private fun mapUrl(message: Message, url: Url): BaseViewModel<*>? {
if (url.ignoreParse || url.meta == null) return null if (url.ignoreParse || url.meta == null) return null
...@@ -313,7 +365,6 @@ class ViewModelMapper @Inject constructor( ...@@ -313,7 +365,6 @@ class ViewModelMapper @Inject constructor(
} }
private fun getSystemMessage(message: Message): CharSequence { private fun getSystemMessage(message: Message): CharSequence {
println(message)
val content = when (message.type) { val content = when (message.type) {
//TODO: Add implementation for Welcome type. //TODO: Add implementation for Welcome type.
is MessageType.MessageRemoved -> context.getString(R.string.message_removed) is MessageType.MessageRemoved -> context.getString(R.string.message_removed)
......
...@@ -9,7 +9,18 @@ import chat.rocket.android.helper.SharedPreferenceHelper ...@@ -9,7 +9,18 @@ import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.helper.UserHelper import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.presentation.MainNavigator import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetActiveUsersInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.JobSchedulerInteractor
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveActiveUsersInteractor
import chat.rocket.android.server.domain.SaveChatRoomsInteractor
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.hasShowLastMessage
import chat.rocket.android.server.domain.showLastMessage
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.infraestructure.ConnectionManager import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.chatRooms import chat.rocket.android.server.infraestructure.chatRooms
...@@ -31,9 +42,13 @@ import chat.rocket.core.internal.rest.permissions ...@@ -31,9 +42,13 @@ import chat.rocket.core.internal.rest.permissions
import chat.rocket.core.internal.rest.spotlight import chat.rocket.core.internal.rest.spotlight
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.CommonPool
import kotlinx.coroutines.experimental.Deferred
import kotlinx.coroutines.experimental.android.UI import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.reflect.KProperty1 import kotlin.reflect.KProperty1
...@@ -43,7 +58,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -43,7 +58,7 @@ class ChatRoomsPresenter @Inject constructor(
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: MainNavigator, private val navigator: MainNavigator,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor, private val chatRoomsInteractor: ChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor, private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val saveActiveUsersInteractor: SaveActiveUsersInteractor, private val saveActiveUsersInteractor: SaveActiveUsersInteractor,
private val getActiveUsersInteractor: GetActiveUsersInteractor, private val getActiveUsersInteractor: GetActiveUsersInteractor,
...@@ -148,7 +163,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -148,7 +163,7 @@ class ChatRoomsPresenter @Inject constructor(
val currentServer = serverInteractor.get()!! val currentServer = serverInteractor.get()!!
launchUI(strategy) { launchUI(strategy) {
try { try {
val roomList = getChatRoomsInteractor.getAllByName(currentServer, name) val roomList = chatRoomsInteractor.getAllByName(currentServer, name)
if (roomList.isEmpty()) { if (roomList.isEmpty()) {
val (users, rooms) = retryIO("spotlight($name)") { val (users, rooms) = retryIO("spotlight($name)") {
client.spotlight(name) client.spotlight(name)
...@@ -253,7 +268,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -253,7 +268,7 @@ class ChatRoomsPresenter @Inject constructor(
fun updateSortedChatRooms() { fun updateSortedChatRooms() {
launchUI(strategy) { launchUI(strategy) {
val roomList = getChatRoomsInteractor.getAll(currentServer) val roomList = chatRoomsInteractor.getAll(currentServer)
view.updateChatRooms(sortRooms(roomList)) view.updateChatRooms(sortRooms(roomList))
} }
} }
...@@ -453,7 +468,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -453,7 +468,7 @@ class ChatRoomsPresenter @Inject constructor(
// 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}") Timber.d("Updating Room: ${room.id} - ${room.name}")
val chatRooms = getChatRoomsInteractor.getAll(currentServer).toMutableList() val chatRooms = chatRoomsInteractor.getAll(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == room.id } val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == room.id }
chatRoom?.apply { chatRoom?.apply {
val newRoom = ChatRoom( val newRoom = ChatRoom(
...@@ -493,7 +508,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -493,7 +508,7 @@ class ChatRoomsPresenter @Inject constructor(
// 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 subscription: ${subscription.id} - ${subscription.name}") Timber.d("Updating subscription: ${subscription.id} - ${subscription.name}")
val chatRooms = getChatRoomsInteractor.getAll(currentServer).toMutableList() val chatRooms = chatRoomsInteractor.getAll(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId } val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId }
chatRoom?.apply { chatRoom?.apply {
val newRoom = ChatRoom( val newRoom = ChatRoom(
...@@ -532,7 +547,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -532,7 +547,7 @@ class ChatRoomsPresenter @Inject constructor(
private fun removeRoom( private fun removeRoom(
id: String, id: String,
chatRooms: MutableList<ChatRoom> = getChatRoomsInteractor.getAll(currentServer).toMutableList() chatRooms: MutableList<ChatRoom> = chatRoomsInteractor.getAll(currentServer).toMutableList()
) { ) {
Timber.d("Removing ROOM: $id") Timber.d("Removing ROOM: $id")
synchronized(this) { synchronized(this) {
...@@ -573,7 +588,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -573,7 +588,7 @@ class ChatRoomsPresenter @Inject constructor(
val username = user_.username val username = user_.username
val status = user_.status val status = user_.status
if (username != null && status != null) { if (username != null && status != null) {
getChatRoomsInteractor.getByName(currentServer, username)?.let { chatRoomsInteractor.getByName(currentServer, username)?.let {
val newRoom = ChatRoom( val newRoom = ChatRoom(
id = it.id, id = it.id,
type = it.type, type = it.type,
...@@ -600,10 +615,10 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -600,10 +615,10 @@ class ChatRoomsPresenter @Inject constructor(
broadcast = it.broadcast broadcast = it.broadcast
) )
getChatRoomsInteractor.remove(currentServer, it) chatRoomsInteractor.remove(currentServer, it)
getChatRoomsInteractor.add(currentServer, newRoom) chatRoomsInteractor.add(currentServer, newRoom)
launchUI(strategy) { launchUI(strategy) {
view.updateChatRooms(sortRooms(getChatRoomsInteractor.getAll(currentServer))) view.updateChatRooms(sortRooms(chatRoomsInteractor.getAll(currentServer)))
} }
} }
} }
...@@ -613,7 +628,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -613,7 +628,7 @@ class ChatRoomsPresenter @Inject constructor(
Timber.i("Updating ChatRooms") Timber.i("Updating ChatRooms")
launch(strategy.jobs) { launch(strategy.jobs) {
val chatRoomsWithPreview = getChatRoomsWithPreviews( val chatRoomsWithPreview = getChatRoomsWithPreviews(
getChatRoomsInteractor.getAll(currentServer) chatRoomsInteractor.getAll(currentServer)
) )
val chatRoomsWithStatus = getChatRoomWithStatus(chatRoomsWithPreview) val chatRoomsWithStatus = getChatRoomWithStatus(chatRoomsWithPreview)
view.updateChatRooms(chatRoomsWithStatus) view.updateChatRooms(chatRoomsWithStatus)
......
...@@ -45,7 +45,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -45,7 +45,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
lateinit var settingsRepository: SettingsRepository lateinit var settingsRepository: SettingsRepository
@Inject @Inject
lateinit var localRepository: LocalRepository lateinit var localRepository: LocalRepository
private lateinit var preferences: SharedPreferences
private var searchView: SearchView? = null private var searchView: SearchView? = null
private val handler = Handler() private val handler = Handler()
...@@ -60,7 +59,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -60,7 +59,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
setHasOptionsMenu(true) setHasOptionsMenu(true)
preferences = context?.getSharedPreferences("temp", Context.MODE_PRIVATE)!!
} }
override fun onDestroy() { override fun onDestroy() {
......
...@@ -2,7 +2,7 @@ package chat.rocket.android.favoritemessages.presentation ...@@ -2,7 +2,7 @@ package chat.rocket.android.favoritemessages.presentation
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.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
...@@ -16,7 +16,7 @@ class FavoriteMessagesPresenter @Inject constructor( ...@@ -16,7 +16,7 @@ class FavoriteMessagesPresenter @Inject constructor(
private val view: FavoriteMessagesView, private val view: FavoriteMessagesView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor, private val roomsInteractor: ChatRoomsInteractor,
private val mapper: ViewModelMapper, private val mapper: ViewModelMapper,
factory: RocketChatClientFactory factory: RocketChatClientFactory
) { ) {
......
...@@ -70,7 +70,7 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView { ...@@ -70,7 +70,7 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
override fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>) { override fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>) {
ui { ui {
if (recycler_view.adapter == null) { if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, null, false) adapter = ChatRoomAdapter(chatRoomType, "", null, false)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val linearLayoutManager = val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
......
package chat.rocket.android.helper
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.useRealName
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Message
import javax.inject.Inject
class MessageHelper @Inject constructor(
getSettingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor
) {
private val currentServer = serverInteractor.get()!!
private val settings: PublicSettings = getSettingsInteractor.get(currentServer)
fun createPermalink(message: Message, chatRoom: ChatRoom): String {
val type = when (chatRoom.type) {
is RoomType.PrivateGroup -> "group"
is RoomType.Channel -> "channel"
is RoomType.DirectMessage -> "direct"
is RoomType.Livechat -> "livechat"
else -> "custom"
}
val name = if (settings.useRealName()) chatRoom.fullName ?: chatRoom.name else chatRoom.name
return "[ ]($currentServer/$type/$name?msg=${message.id}) "
}
fun messageIdFromPermalink(permalink: String): String? {
PERMALINK_REGEX.find(permalink.trim())?.let {
if (it.groupValues.size == 5) {
return it.groupValues[MESSAGE_ID]
}
}
return null
}
fun roomNameFromPermalink(permalink: String): String? {
PERMALINK_REGEX.find(permalink.trim())?.let {
if (it.groupValues.size == 5) {
return it.groupValues[ROOM_NAME]
}
}
return null
}
fun roomTypeFromPermalink(permalink: String): String? {
PERMALINK_REGEX.find(permalink.trim())?.let {
if (it.groupValues.size == 5) {
val type = it.groupValues[ROOM_TYPE]
return when(type) {
"group" -> "p"
"channel" -> "c"
"direct" -> "d"
"livechat" -> "l"
else -> type
}
}
}
return null
}
companion object {
private const val ROOM_TYPE = 2
private const val ROOM_NAME = 3
private const val MESSAGE_ID = 4
val PERMALINK_REGEX = "(?:__|[*#])|\\[(.+?)\\]\\(.+?//.+?/(.+)/(.+)\\?.*=(.*)\\)".toRegex()
}
}
\ No newline at end of file
...@@ -48,7 +48,7 @@ class UserHelper @Inject constructor( ...@@ -48,7 +48,7 @@ class UserHelper @Inject constructor(
/** /**
* Return the username for the current logged [User]. * Return the username for the current logged [User].
*/ */
fun username(): String? = user()?.username fun username(): String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY, null)
/** /**
* Whether current [User] is admin on the current server. * Whether current [User] is admin on the current server.
......
package chat.rocket.android.main.presentation package chat.rocket.android.main.presentation
import android.content.Context
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.ui.newServerIntent import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.chatroom.ui.chatRoomIntent import chat.rocket.android.chatroom.ui.chatRoomIntent
...@@ -37,9 +36,9 @@ class MainNavigator(internal val activity: MainActivity) { ...@@ -37,9 +36,9 @@ class MainNavigator(internal val activity: MainActivity) {
isChatRoomReadOnly: Boolean, isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long, chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean, isChatRoomSubscribed: Boolean,
isChatRoomOwner: Boolean) { isChatRoomCreator: Boolean) {
activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType, activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomOwner)) isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomCreator))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit) activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
} }
......
...@@ -2,7 +2,7 @@ package chat.rocket.android.pinnedmessages.presentation ...@@ -2,7 +2,7 @@ package chat.rocket.android.pinnedmessages.presentation
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.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
...@@ -17,7 +17,7 @@ class PinnedMessagesPresenter @Inject constructor( ...@@ -17,7 +17,7 @@ class PinnedMessagesPresenter @Inject constructor(
private val view: PinnedMessagesView, private val view: PinnedMessagesView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor, private val roomsInteractor: ChatRoomsInteractor,
private val mapper: ViewModelMapper, private val mapper: ViewModelMapper,
factory: RocketChatClientFactory factory: RocketChatClientFactory
) { ) {
......
...@@ -73,7 +73,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -73,7 +73,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) { override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
ui { ui {
if (recycler_view_pinned.adapter == null) { if (recycler_view_pinned.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, null, false) adapter = ChatRoomAdapter(chatRoomType, "", null, false)
recycler_view_pinned.adapter = adapter recycler_view_pinned.adapter = adapter
val linearLayoutManager = val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
......
...@@ -5,7 +5,7 @@ import kotlinx.coroutines.experimental.CommonPool ...@@ -5,7 +5,7 @@ import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject import javax.inject.Inject
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) { class ChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
/** /**
* Get all [ChatRoom]. * Get all [ChatRoom].
...@@ -41,8 +41,7 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo ...@@ -41,8 +41,7 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo
* @return The [ChatRoom] object or null if we couldn't find any. * @return The [ChatRoom] object or null if we couldn't find any.
*/ */
suspend fun getById(serverUrl: String, roomId: String): ChatRoom? = withContext(CommonPool) { suspend fun getById(serverUrl: String, roomId: String): ChatRoom? = withContext(CommonPool) {
val allChatRooms = repository.get(serverUrl) return@withContext repository.get(serverUrl).find {
return@withContext allChatRooms.first {
it.id == roomId it.id == roomId
} }
} }
...@@ -55,7 +54,7 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo ...@@ -55,7 +54,7 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo
* @return The [ChatRoom] object or null if we couldn't find any. * @return The [ChatRoom] object or null if we couldn't find any.
*/ */
fun getByName(serverUrl: String, name: String): ChatRoom? { fun getByName(serverUrl: String, name: String): ChatRoom? {
return getAll(serverUrl).toMutableList().find { chatRoom -> chatRoom.name == name } return getAll(serverUrl).firstOrNull { it.name == name || it.fullName == name }
} }
/** /**
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M22.548,9l0.452,-2h-5.364l1.364,-6h-2l-1.364,6h-5l1.364,-6h-2l-1.364,6h-6.184l-0.452,2h6.182l-1.364,6h-5.36l-0.458,2h5.364l-1.364,6h2l1.364,-6h5l-1.364,6h2l1.364,-6h6.185l0.451,-2h-6.182l1.364,-6h5.366zM13.818,15h-5l1.364,-6h5l-1.364,6z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="20.0"
android:viewportWidth="20.0">
<path
android:fillColor="#FF000000"
android:pathData="M13.6,13.47c-0.91,0.953 -2.191,1.545 -3.61,1.545 -2.756,0 -4.99,-2.234 -4.99,-4.99 0,-0.009 0,-0.018 0,-0.026v0.001c0,-2.761 2.239,-5 5,-5 1.131,0 2.175,0.376 3.013,1.009l-0.013,-0.009v-1h2v6.5c0,0.828 0.672,1.5 1.5,1.5s1.5,-0.672 1.5,-1.5v0,-1.5c-0.003,-4.416 -3.584,-7.994 -8,-7.994 -4.418,0 -8,3.582 -8,8s3.582,8 8,8c1.305,0 2.537,-0.312 3.625,-0.867l-0.045,0.021 0.9,1.79c-1.305,0.668 -2.847,1.06 -4.48,1.06 -5.523,0 -10,-4.477 -10,-10s4.477,-10 10,-10c5.519,0 9.994,4.472 10,9.99v0.001h-0.01v1.5c0,0.003 0,0.007 0,0.01 0,1.933 -1.567,3.5 -3.5,3.5 -1.202,0 -2.262,-0.606 -2.892,-1.528l-0.008,-0.012zM10,13c1.657,0 3,-1.343 3,-3s-1.343,-3 -3,-3v0c-1.657,0 -3,1.343 -3,3s1.343,3 3,3v0z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M18,10v-4c0,-3.313 -2.687,-6 -6,-6s-6,2.687 -6,6v4h-3v14h18v-14h-3zM8,6c0,-2.206 1.794,-4 4,-4s4,1.794 4,4v4h-8v-4zM19,22h-14v-10h14v10z" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="4dp" />
<stroke android:color="#1D74F5" android:width="2dp" />
</shape>
\ No newline at end of file
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<include <include
android:id="@+id/layout_app_bar" android:id="@+id/layout_app_bar"
layout="@layout/app_bar" /> layout="@layout/app_bar_chat_room" />
<FrameLayout <FrameLayout
android:id="@+id/fragment_container" android:id="@+id/fragment_container"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout 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:background="@color/colorPrimary"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationIcon="?android:attr/homeAsUpIndicator"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.constraint.ConstraintLayout
android:id="@+id/toolbar_content_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- TODO implement -->
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_room_avatar"
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:roundAsCircle="true" />
<TextView
android:id="@+id/text_room_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:drawablePadding="@dimen/text_view_drawable_padding"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Developers" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
\ 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"
android:id="@+id/message_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding">
<Button
android:id="@+id/button_message_reply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginTop="5dp"
android:layout_marginStart="56dp"
android:background="@drawable/message_reply_button_bg"
android:text="@string/action_msg_reply"
android:textAllCaps="false"
android:textColor="#1D74F5"
android:textSize="14sp"
app:layout_constraintLeft_toLeftOf="parent" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@+id/button_message_reply"
app:layout_constraintStart_toStartOf="@+id/button_message_reply"
app:layout_constraintTop_toBottomOf="@+id/button_message_reply" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
package chat.rocket.android
import chat.rocket.android.server.infraestructure.MemoryMessagesRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
import kotlinx.coroutines.experimental.runBlocking
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Test
import org.hamcrest.CoreMatchers.`is` as isEqualTo
class MemoryMessagesRepositoryTest {
val repository = MemoryMessagesRepository()
val msg = Message(
id = "messageId",
roomId = "GENERAL",
message = "Beam me up, Scotty.",
timestamp = 1511443964815,
attachments = null,
sender = null,
avatar = null,
channels = null,
editedAt = null,
editedBy = null,
groupable = true,
mentions = null,
parseUrls = false,
senderAlias = null,
type = MessageType.MessageRemoved(),
updatedAt = 1511443964815,
urls = null,
pinned = false,
reactions = null
)
val msg2 = Message(
id = "messageId2",
roomId = "sandbox",
message = "Highly Illogical",
timestamp = 1511443964818,
attachments = null,
sender = null,
avatar = null,
channels = null,
editedAt = null,
editedBy = null,
groupable = true,
mentions = null,
parseUrls = false,
senderAlias = null,
type = MessageType.MessageRemoved(),
updatedAt = 1511443964818,
urls = null,
pinned = false,
reactions = null
)
@Before
fun setup() {
runBlocking {
repository.clear()
}
}
@Test
fun `save() should save a single message`() {
runBlocking {
assertThat(repository.getAll().size, isEqualTo(0))
repository.save(msg)
val allMessages = repository.getAll()
assertThat(allMessages.size, isEqualTo(1))
allMessages[0].apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
}
}
@Test
fun `saveAll() should all saved messages`() {
runBlocking {
assertThat(repository.getAll().size, isEqualTo(0))
repository.saveAll(listOf(msg, msg2))
val allMessages = repository.getAll()
assertThat(allMessages.size, isEqualTo(2))
allMessages[0].apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
allMessages[1].apply {
assertThat(id, isEqualTo("messageId2"))
assertThat(message, isEqualTo("Highly Illogical"))
assertThat(roomId, isEqualTo("sandbox"))
}
}
}
@Test
fun `getById() should return a single message`() {
runBlocking {
repository.saveAll(listOf(msg, msg2))
var singleMsg = repository.getById("messageId")
assertThat(singleMsg, notNullValue())
singleMsg!!.apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
singleMsg = repository.getById("messageId2")
assertThat(singleMsg, notNullValue())
singleMsg!!.apply {
assertThat(id, isEqualTo("messageId2"))
assertThat(message, isEqualTo("Highly Illogical"))
assertThat(roomId, isEqualTo("sandbox"))
}
}
}
@Test
fun `getByRoomId() should return all messages for room id or an empty list`() {
runBlocking {
repository.saveAll(listOf(msg, msg2))
var roomMessages = repository.getByRoomId("faAad32fkasods2")
assertThat(roomMessages.isEmpty(), isEqualTo(true))
roomMessages = repository.getByRoomId("sandbox")
assertThat(roomMessages.size, isEqualTo(1))
roomMessages[0].apply {
assertThat(id, isEqualTo("messageId2"))
assertThat(message, isEqualTo("Highly Illogical"))
assertThat(roomId, isEqualTo("sandbox"))
}
roomMessages = repository.getByRoomId("GENERAL")
assertThat(roomMessages.size, isEqualTo(1))
roomMessages[0].apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
}
}
}
\ No newline at end of file
...@@ -4,3 +4,4 @@ distributionPath=wrapper/dists ...@@ -4,3 +4,4 @@ distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
distributionSha256Sum=9af7345c199f1731c187c96d3fe3d31f5405192a42046bafa71d846c3d9adacb
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