Unverified Commit 5e5e565d authored by Rafael Kellermann Streit's avatar Rafael Kellermann Streit Committed by GitHub

Merge pull request #1287 from RocketChat/beta

[RELEASE] Merge beta into master
parents b0817ace 744cdeb2
......@@ -13,8 +13,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 2020
versionName "2.1.1"
versionCode 2021
versionName "2.2.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......
......@@ -18,7 +18,6 @@ import com.google.android.flexbox.FlexboxLayoutManager
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
abstract class BaseViewHolder<T : BaseViewModel<*>>(
itemView: View,
private val listener: ActionsListener,
......@@ -76,22 +75,29 @@ abstract class BaseViewHolder<T : BaseViewModel<*>>(
fun onActionSelected(item: MenuItem, message: Message)
}
private val longClickListener = { view: View ->
private val onClickListener = { view: View ->
if (data?.message?.isSystemMessage() == false) {
data?.message?.let {
val menuItems = view.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = data?.message?.pinned ?: false
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = isPinned
menuItems.find { it.itemId == R.id.action_message_unpin }?.apply {
setTitle(if (it.pinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = it.pinned
}
menuItems.find { it.itemId == R.id.action_message_star }?.apply {
val isStarred = it.starred?.isNotEmpty() ?: false
setTitle(if (isStarred) R.string.action_msg_unstar else R.string.action_msg_star)
isChecked = isStarred
}
val adapter = ActionListAdapter(menuItems, this@BaseViewHolder)
BottomSheetMenu(adapter).show(view.context)
}
true
}
}
internal fun setupActionMenu(view: View) {
if (listener.isActionsEnabled()) {
view.setOnClickListener(onClickListener)
if (view is ViewGroup) {
for (child in view.children) {
if (child !is RecyclerView && child.id != R.id.recycler_view_reactions) {
......@@ -99,7 +105,6 @@ abstract class BaseViewHolder<T : BaseViewModel<*>>(
}
}
}
view.setOnLongClickListener(longClickListener)
}
}
......
......@@ -5,7 +5,21 @@ import android.view.MenuItem
import android.view.ViewGroup
import chat.rocket.android.R
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.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.Message
......@@ -65,6 +79,12 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_file_attachment)
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 -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
......@@ -98,15 +118,26 @@ class ChatRoomAdapter(
}
when (holder) {
is MessageViewHolder -> holder.bind(dataSet[position] as MessageViewModel)
is ImageAttachmentViewHolder -> holder.bind(dataSet[position] as ImageAttachmentViewModel)
is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel)
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
is AuthorAttachmentViewHolder -> holder.bind(dataSet[position] as AuthorAttachmentViewModel)
is ColorAttachmentViewHolder -> holder.bind(dataSet[position] as ColorAttachmentViewModel)
is GenericFileAttachmentViewHolder -> holder.bind(dataSet[position] as GenericFileAttachmentViewModel)
is MessageViewHolder ->
holder.bind(dataSet[position] as MessageViewModel)
is ImageAttachmentViewHolder ->
holder.bind(dataSet[position] as ImageAttachmentViewModel)
is AudioAttachmentViewHolder ->
holder.bind(dataSet[position] as AudioAttachmentViewModel)
is VideoAttachmentViewHolder ->
holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder ->
holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder ->
holder.bind(dataSet[position] as MessageAttachmentViewModel)
is AuthorAttachmentViewHolder ->
holder.bind(dataSet[position] as AuthorAttachmentViewModel)
is ColorAttachmentViewHolder ->
holder.bind(dataSet[position] as ColorAttachmentViewModel)
is GenericFileAttachmentViewHolder ->
holder.bind(dataSet[position] as GenericFileAttachmentViewModel)
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyViewModel)
}
}
......@@ -181,25 +212,39 @@ class ChatRoomAdapter(
}
private val actionsListener = object : BaseViewHolder.ActionsListener {
override fun isActionsEnabled(): Boolean = enableActions
override fun onActionSelected(item: MenuItem, message: Message) {
message.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter?.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter?.citeMessage(roomType, id, false)
R.id.action_menu_msg_reply -> presenter?.citeMessage(roomType, id, true)
R.id.action_menu_msg_copy -> presenter?.copyMessage(id)
R.id.action_menu_msg_edit -> presenter?.editMessage(roomId, id, message.message)
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
R.id.action_message_reply -> {
presenter?.citeMessage(roomName, roomType, id, true)
}
R.id.action_message_quote -> {
presenter?.citeMessage(roomName, roomType, id, false)
}
R.id.action_message_copy -> {
presenter?.copyMessage(id)
}
R.id.action_message_edit -> {
presenter?.editMessage(roomId, id, message.message)
}
R.id.action_message_star -> {
if (!item.isChecked) {
presenter?.starMessage(id)
} else {
presenter?.unstarMessage(id)
}
}
R.id.action_message_unpin -> {
if (!item.isChecked) {
presenter?.pinMessage(id)
} else {
presenter?.unpinMessage(id)
}
}
}
R.id.action_message_delete -> presenter?.deleteMessage(roomId, id)
R.id.action_menu_msg_react -> presenter?.showReactions(id)
else -> TODO("Not implemented")
}
......
......@@ -37,11 +37,11 @@ import timber.log.Timber
import java.io.File
class ImageAttachmentViewHolder(itemView: View,
class ImageAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
private var cacheKey: CacheKey? = null
init {
......@@ -63,7 +63,8 @@ class ImageAttachmentViewHolder(itemView: View,
// TODO - implement a proper image viewer with a proper Transition
// TODO - We should definitely write our own ImageViewer
var imageViewer: ImageViewer? = null
val request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(data.attachmentUrl))
val request =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(data.attachmentUrl))
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.DISK_CACHE)
.build()
......@@ -71,8 +72,10 @@ class ImageAttachmentViewHolder(itemView: View,
.getEncodedCacheKey(request, null)
val pad = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_padding)
val lparams = AppBarLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
val lparams = AppBarLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val toolbar = Toolbar(context).also {
it.inflateMenu(R.menu.image_actions)
it.overflowIcon?.setTint(Color.WHITE)
......@@ -98,7 +101,7 @@ class ImageAttachmentViewHolder(itemView: View,
val backArrowView = ImageView(context).also {
it.setImageResource(R.drawable.ic_arrow_back_white_24dp)
it.setOnClickListener { imageViewer?.onDismiss() }
it.setPadding(0, pad ,pad, pad)
it.setPadding(0, pad, pad, pad)
}
val layoutParams = AppBarLayout.LayoutParams(
......@@ -113,10 +116,12 @@ class ImageAttachmentViewHolder(itemView: View,
val appBarLayout = AppBarLayout(context).also {
it.layoutParams = lparams
it.setBackgroundColor(Color.BLACK)
it.addView(toolbar, AppBarLayout.LayoutParams(
it.addView(
toolbar, AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
))
)
)
}
val builder = ImageViewer.createPipelineDraweeControllerBuilder()
......@@ -144,12 +149,17 @@ class ImageAttachmentViewHolder(itemView: View,
val imageFormat = ImageFormatChecker.getImageFormat(resource.openStream())
val imageDir = "${Environment.DIRECTORY_PICTURES}/Rocket.Chat Images/"
val imagePath = Environment.getExternalStoragePublicDirectory(imageDir)
val imageFile = File(imagePath, "${cachedFile.nameWithoutExtension}.${imageFormat.fileExtension}")
val imageFile =
File(imagePath, "${cachedFile.nameWithoutExtension}.${imageFormat.fileExtension}")
imagePath.mkdirs()
imageFile.createNewFile()
try {
cachedFile.copyTo(imageFile, true)
MediaScannerConnection.scanFile(context, arrayOf(imageFile.absolutePath), null) { path, uri ->
MediaScannerConnection.scanFile(
context,
arrayOf(imageFile.absolutePath),
null
) { path, uri ->
Timber.i("Scanned $path:")
Timber.i("-> uri=$uri")
}
......@@ -166,16 +176,21 @@ class ImageAttachmentViewHolder(itemView: View,
}
private fun canWriteToExternalStorage(): Boolean {
return AndroidPermissionsHelper.checkPermission(itemView.context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
return AndroidPermissionsHelper.checkPermission(
itemView.context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
private fun checkWritingPermission() {
val context = itemView.context
if (context is ContextThemeWrapper && context.baseContext is Activity) {
val activity = context.baseContext as Activity
AndroidPermissionsHelper.requestPermission(activity,
AndroidPermissionsHelper.requestPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
AndroidPermissionsHelper.WRITE_EXTERNAL_STORAGE_CODE)
AndroidPermissionsHelper.WRITE_EXTERNAL_STORAGE_CODE
)
}
}
}
\ No newline at end of file
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
......@@ -3,8 +3,11 @@ package chat.rocket.android.chatroom.adapter
import android.graphics.Color
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.isSystemMessage
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
......@@ -33,6 +36,10 @@ class MessageViewHolder(
text_content.setTextColor(
if (data.isTemporary) Color.GRAY else Color.BLACK
)
data.message.let {
text_edit_indicator.isVisible = it.isSystemMessage() && it.editedBy != null
image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false
}
}
}
}
\ No newline at end of file
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
import chat.rocket.android.R
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.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack
......@@ -15,8 +16,14 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
}
fun toPinnedMessageList(chatRoomId: String, chatRoomType: String) {
activity.addFragmentBackStack("PinnedMessages", R.id.fragment_container){
chat.rocket.android.pinnedmessages.ui.newInstance(chatRoomId,chatRoomType)
activity.addFragmentBackStack("PinnedMessages", R.id.fragment_container) {
chat.rocket.android.pinnedmessages.ui.newInstance(chatRoomId, chatRoomType)
}
}
fun toFavoriteMessageList(chatRoomId: String, chatRoomType: String) {
activity.addFragmentBackStack("FavoriteMessages", R.id.fragment_container) {
chat.rocket.android.favoritemessages.ui.newInstance(chatRoomId, chatRoomType)
}
}
......@@ -24,4 +31,17 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
activity.startActivity(activity.changeServerIntent())
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,15 +6,28 @@ import chat.rocket.android.chatroom.adapter.AutoCompleteType
import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.ROOMS
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.suggestion.ChatRoomSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.MessageHelper
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.username
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.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.state
import chat.rocket.android.util.extensions.avatarUrl
......@@ -26,8 +39,30 @@ import chat.rocket.common.model.SimpleUser
import chat.rocket.common.model.UserStatus
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.setTypingStatus
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.rest.*
import chat.rocket.core.internal.realtime.subscribeTypingStatus
import chat.rocket.core.internal.realtime.unsubscribe
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.Message
import chat.rocket.core.model.Myself
......@@ -45,30 +80,69 @@ class ChatRoomPresenter @Inject constructor(
private val view: ChatRoomView,
private val navigator: ChatRoomNavigator,
private val strategy: CancelStrategy,
getSettingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val permissions: GetPermissionsInteractor,
private val chatRoomsInteractor: ChatRoomsInteractor,
private val permissions: PermissionsInteractor,
private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository,
private val usersRepository: UsersRepository,
private val roomsRepository: RoomRepository,
private val localRepository: LocalRepository,
factory: ConnectionManagerFactory,
private val userHelper: UserHelper,
private val mapper: ViewModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor
private val jobSchedulerInteractor: JobSchedulerInteractor,
private val messageHelper: MessageHelper,
getSettingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor,
factory: ConnectionManagerFactory
) {
private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer)
private val client = manager.client
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
private val currentLoggedUsername = userHelper.username()
private val messagesChannel = Channel<Message>()
private var chatRoomId: String? = null
private var chatRoomType: String? = null
private var chatIsBroadcast: Boolean = false
private var chatRoles = emptyList<ChatRoomRole>()
private val stateChannel = Channel<State>()
private var typingStatusSubscriptionId: String? = null
private var lastState = manager.state
private var typingStatusList = arrayListOf<String>()
fun setupChatRoom(roomId: String, roomName: String, roomType: String, chatRoomMessage: String? = null) {
launchUI(strategy) {
try {
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) {
this.chatRoomId = chatRoomId
......@@ -78,7 +152,8 @@ class ChatRoomPresenter @Inject constructor(
try {
if (offset == 0L) {
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()) {
view.showMessages(oldMessages)
loadMissingMessages()
......@@ -105,6 +180,7 @@ class ChatRoomPresenter @Inject constructor(
view.hideLoading()
}
subscribeTypingStatus()
if (offset == 0L) {
subscribeState()
}
......@@ -117,8 +193,8 @@ class ChatRoomPresenter @Inject constructor(
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
}
messagesRepository.saveAll(messages)
val allMessages = mapper.map(messages)
view.showMessages(allMessages)
view.showMessages(mapper.map(messages, RoomViewModel(roles = chatRoles,
isBroadcast = chatIsBroadcast, isRoom = true)))
}
fun sendMessage(chatRoomId: String, text: String, messageId: String?) {
......@@ -127,7 +203,7 @@ class ChatRoomPresenter @Inject constructor(
// ignore message for now, will receive it on the stream
val id = UUID.randomUUID().toString()
val message = if (messageId == null) {
val username = localRepository.username()
val username = userHelper.username()
val newMessage = Message(
id = id,
roomId = chatRoomId,
......@@ -142,6 +218,7 @@ class ChatRoomPresenter @Inject constructor(
groupable = false,
parseUrls = false,
pinned = false,
starred = emptyList(),
mentions = emptyList(),
reactions = null,
senderAlias = null,
......@@ -151,9 +228,10 @@ class ChatRoomPresenter @Inject constructor(
isTemporary = true
)
try {
val message = client.sendMessage(id, chatRoomId, text)
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
} catch (ex: Exception) {
// Ok, not very beautiful, but the backend sends us a not valid response
......@@ -219,6 +297,22 @@ class ChatRoomPresenter @Inject constructor(
}
}
fun sendTyping() {
launch(CommonPool + strategy.jobs) {
if (chatRoomId != null && currentLoggedUsername != null) {
client.setTypingStatus(chatRoomId.toString(), currentLoggedUsername, true)
}
}
}
fun sendNotTyping() {
launch(CommonPool + strategy.jobs) {
if (chatRoomId != null && currentLoggedUsername != null) {
client.setTypingStatus(chatRoomId.toString(), currentLoggedUsername, false)
}
}
}
private fun markRoomAsRead(roomId: String) {
launchUI(strategy) {
try {
......@@ -278,7 +372,8 @@ class ChatRoomPresenter @Inject constructor(
Timber.d("History: $messages")
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)
launchUI(strategy) {
......@@ -300,14 +395,6 @@ class ChatRoomPresenter @Inject constructor(
}
}
fun unsubscribeMessages(chatRoomId: String) {
manager.removeStatusChannel(stateChannel)
manager.unsubscribeRoomMessages(chatRoomId)
// All messages during the subscribed period are assumed to be read,
// and lastSeen is updated as the time when the user leaves the room
markRoomAsRead(chatRoomId)
}
/**
* Delete the message with the given id.
*
......@@ -343,7 +430,7 @@ class ChatRoomPresenter @Inject constructor(
* @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.
*/
fun citeMessage(roomType: String, messageId: String, mentionAuthor: Boolean) {
fun citeMessage(roomName: String, roomType: String, messageId: String, mentionAuthor: Boolean) {
launchUI(strategy) {
val message = messagesRepository.getById(messageId)
val me: Myself? = try {
......@@ -356,11 +443,19 @@ class ChatRoomPresenter @Inject constructor(
val id = msg.id
val username = msg.sender?.username ?: ""
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(
username = getDisplayName(msg.sender),
replyMarkdown = "[ ]($currentServer/$roomType/$room?msg=$id) $mention ",
quotedMessage = mapper.map(message).last().preview?.message ?: ""
replyMarkdown = "[ ]($currentServer/$chatRoomType/$room?msg=$id) $mention ",
quotedMessage = mapper.map(message, RoomViewModel(roles = chatRoles,
isBroadcast = chatIsBroadcast)).last().preview?.message ?: ""
)
}
}
......@@ -405,6 +500,34 @@ class ChatRoomPresenter @Inject constructor(
}
}
fun starMessage(messageId: String) {
launchUI(strategy) {
if (!permissions.allowedMessageStarring()) {
view.showMessage(R.string.permission_starring_not_allowed)
return@launchUI
}
try {
retryIO("starMessage($messageId)") { client.starMessage(messageId) }
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
fun unstarMessage(messageId: String) {
launchUI(strategy) {
if (!permissions.allowedMessageStarring()) {
view.showMessage(R.string.permission_starring_not_allowed)
return@launchUI
}
try {
retryIO("unstarMessage($messageId)") { client.unstarMessage(messageId) }
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
fun pinMessage(messageId: String) {
launchUI(strategy) {
if (!permissions.allowedMessagePinning()) {
......@@ -515,14 +638,19 @@ class ChatRoomPresenter @Inject constructor(
}
}
fun toMembersList(chatRoomId: String, chatRoomType: String) = navigator.toMembersList(chatRoomId, chatRoomType)
fun toMembersList(chatRoomId: String, chatRoomType: String) =
navigator.toMembersList(chatRoomId, chatRoomType)
fun toPinnedMessageList(chatRoomId: String, chatRoomType: String) =
navigator.toPinnedMessageList(chatRoomId, chatRoomType)
fun toPinnedMessageList(chatRoomId: String, chatRoomType: String) = navigator.toPinnedMessageList(chatRoomId,chatRoomType)
fun toFavoriteMessageList(chatRoomId: String, chatRoomType: String) =
navigator.toFavoriteMessageList(chatRoomId, chatRoomType)
fun loadChatRooms() {
launchUI(strategy) {
try {
val chatRooms = getChatRoomsInteractor.getAll(currentServer)
val chatRooms = chatRoomsInteractor.getAll(currentServer)
.filterNot {
it.type is RoomType.DirectMessage || it.type is RoomType.Livechat
}
......@@ -547,20 +675,47 @@ class ChatRoomPresenter @Inject constructor(
launchUI(strategy) {
try {
retryIO("joinChat($chatRoomId)") { client.joinChat(chatRoomId) }
view.onJoined()
val canPost = permissions.canPostToReadOnlyChannels()
view.onJoined(canPost)
} catch (ex: RocketChatException) {
Timber.e(ex)
}
}
}
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.
*/
fun react(messageId: String, emoji: String) {
launchUI(strategy) {
try {
retryIO("toogleEmoji($messageId, $emoji)") {
retryIO("toggleEmoji($messageId, $emoji)") {
client.toggleReaction(messageId, emoji.removeSurrounding(":"))
}
} catch (ex: RocketChatException) {
......@@ -624,9 +779,61 @@ class ChatRoomPresenter @Inject constructor(
}
}
fun disconnect() {
unsubscribeTypingStatus()
if (chatRoomId != null) {
unsubscribeMessages(chatRoomId.toString())
}
}
private suspend fun subscribeTypingStatus() {
client.subscribeTypingStatus(chatRoomId.toString()) { _, id ->
typingStatusSubscriptionId = id
}
for (typingStatus in client.typingStatusChannel) {
processTypingStatus(typingStatus)
}
}
private fun processTypingStatus(typingStatus: Pair<String, Boolean>) {
if (!typingStatusList.any { username -> username == typingStatus.first }) {
if (typingStatus.second) {
typingStatusList.add(typingStatus.first)
}
} else {
typingStatusList.find { username -> username == typingStatus.first }?.let {
typingStatusList.remove(it)
if (typingStatus.second) {
typingStatusList.add(typingStatus.first)
}
}
}
if (typingStatusList.isNotEmpty()) {
view.showTypingStatus(typingStatusList)
} else {
view.hideTypingStatusView()
}
}
private fun unsubscribeTypingStatus() {
typingStatusSubscriptionId?.let {
client.unsubscribe(it)
}
}
private fun unsubscribeMessages(chatRoomId: String) {
manager.removeStatusChannel(stateChannel)
manager.unsubscribeRoomMessages(chatRoomId)
// All messages during the subscribed period are assumed to be read,
// and lastSeen is updated as the time when the user leaves the room
markRoomAsRead(chatRoomId)
}
private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) {
val viewModelStreamedMessage = mapper.map(streamedMessage)
val viewModelStreamedMessage = mapper.map(streamedMessage, RoomViewModel(
roles = chatRoles, isBroadcast = chatIsBroadcast))
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index > -1) {
......
......@@ -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.MessageView
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
interface ChatRoomView : LoadingView, MessageView {
......@@ -25,6 +26,18 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun sendMessage(text: String)
/**
* Shows the username(s) of the user(s) who is/are typing in the chat room.
*
* @param usernameList The list of username to show.
*/
fun showTypingStatus(usernameList: ArrayList<String>)
/**
* Hides the typing status view.
*/
fun hideTypingStatusView()
/**
* Perform file selection with the mime type [filter]
*/
......@@ -109,8 +122,10 @@ interface ChatRoomView : LoadingView, MessageView {
fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>)
/**
* This user has joined the chat callback.
*
* @param userCanPost Whether the user can post a message or not.
*/
fun onJoined()
fun onJoined(userCanPost: Boolean)
fun showReactionsPopup(messageId: String)
......@@ -120,4 +135,15 @@ interface ChatRoomView : LoadingView, MessageView {
* @param commands The list of available commands.
*/
fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>)
/**
* 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() {
)
messageRepository.save(message.copy(isTemporary = false))
Timber.d("Sent scheduled message given by id: ${message.id}")
} catch (ex: RocketChatException) {
} catch (ex: Exception) {
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) {
// XXX: Temporary solution. We need proper error codes from the api.
messageRepository.save(message.copy(isTemporary = false))
......@@ -71,6 +79,7 @@ class MessageService : JobService() {
}
}
}
}
companion object {
const val RETRY_SEND_MESSAGE_ID = 1
......
package chat.rocket.android.chatroom.ui
import DrawableHelper
import android.content.Context
import android.content.Intent
import android.os.Bundle
......@@ -27,38 +26,49 @@ fun Context.chatRoomIntent(
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean = true
isChatRoomSubscribed: Boolean = true,
isChatRoomCreator: Boolean = false,
chatRoomMessage: String? = null
): Intent {
return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
putExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putExtra(INTENT_CHAT_IS_SUBSCRIBED, isChatRoomSubscribed)
putExtra(INTENT_CHAT_ROOM_IS_CREATOR, isChatRoomCreator)
putExtra(INTENT_CHAT_ROOM_MESSAGE, chatRoomMessage)
}
}
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_TYPE = "chat_room_type"
private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val INTENT_CHAT_ROOM_IS_READ_ONLY = "chat_room_is_read_only"
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_IS_SUBSCRIBED = "is_chat_room_subscribed"
private const val INTENT_CHAT_ROOM_MESSAGE = "chat_room_message"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
// TODO - workaround for now... We will move to a single activity
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var navigator: ChatRoomNavigator
@Inject lateinit var managerFactory: ConnectionManagerFactory
@Inject
lateinit var serverInteractor: GetCurrentServerInteractor
@Inject
lateinit var navigator: ChatRoomNavigator
@Inject
lateinit var managerFactory: ConnectionManagerFactory
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private var isChatRoomSubscribed: Boolean = true
private var isChatRoomCreator: Boolean = false
private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) {
......@@ -84,8 +94,13 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, true)
requireNotNull(chatRoomType) { "no is_chat_room_read_only provided in Intent extras" }
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true)
requireNotNull(isChatRoomReadOnly) { "no chat_room_is_read_only provided in Intent extras" }
isChatRoomCreator = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_CREATOR, false)
requireNotNull(isChatRoomCreator) { "no chat_room_is_creator provided in Intent extras" }
val chatRoomMessage = intent.getStringExtra(INTENT_CHAT_ROOM_MESSAGE)
setupToolbar()
......@@ -96,7 +111,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
if (supportFragmentManager.findFragmentByTag(TAG_CHAT_ROOM_FRAGMENT) == null) {
addFragment(TAG_CHAT_ROOM_FRAGMENT, R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
isChatRoomSubscribed)
isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage)
}
}
}
......@@ -109,6 +124,17 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
text_room_name.textContent = chatRoomName
showRoomTypeIcon(true)
toolbar.setNavigationOnClickListener { finishActivity() }
}
fun showRoomTypeIcon(showRoomTypeIcon: Boolean) {
if (showRoomTypeIcon) {
val roomType = roomTypeOf(chatRoomType)
......@@ -136,17 +162,6 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
}
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName
showRoomTypeIcon(true)
toolbar.setNavigationOnClickListener {
finishActivity()
}
}
fun setupToolbarTitle(toolbarTitle: String) {
text_room_name.textContent = toolbarTitle
......
......@@ -13,9 +13,22 @@ import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.*
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 androidx.core.text.bold
import androidx.core.view.isVisible
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.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
......@@ -26,15 +39,37 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewMod
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.emoji.*
import chat.rocket.android.util.extensions.asObservable
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.model.ChatRoom
import dagger.android.support.AndroidSupportInjection
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
......@@ -44,7 +79,9 @@ fun newInstance(
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isSubscribed: Boolean = true
isSubscribed: Boolean = true,
isChatRoomCreator: Boolean = false,
chatRoomMessage: String? = null
): Fragment {
return ChatRoomFragment().apply {
arguments = Bundle(1).apply {
......@@ -54,6 +91,8 @@ fun newInstance(
putBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putLong(BUNDLE_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED, isSubscribed)
putBoolean(BUNDLE_CHAT_ROOM_IS_CREATOR, isChatRoomCreator)
putString(BUNDLE_CHAT_ROOM_MESSAGE, chatRoomMessage)
}
}
}
......@@ -65,8 +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 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_CREATOR = "chat_room_is_creator"
private const val BUNDLE_CHAT_ROOM_MESSAGE = "chat_room_message"
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
@Inject
lateinit var presenter: ChatRoomPresenter
@Inject
......@@ -75,8 +117,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var chatRoomMessage: String? = null
private var isSubscribed: Boolean = true
private var isChatRoomReadOnly: Boolean = false
private var isChatRoomCreator: Boolean = false
private var isBroadcastChannel: Boolean = false
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var chatRoomLastSeen: Long = -1
private lateinit var actionSnackbar: ActionSnackbar
......@@ -87,8 +132,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private var playComposeMessageButtonsAnimation = true
// For reveal and unreveal anim.
private val hypotenuse by lazy { Math.hypot(root_layout.width.toDouble(), root_layout.height.toDouble()).toFloat() }
private val max by lazy { Math.max(layout_message_attachment_options.width.toDouble(), layout_message_attachment_options.height.toDouble()).toFloat() }
private val hypotenuse by lazy {
Math.hypot(
root_layout.width.toDouble(),
root_layout.height.toDouble()
).toFloat()
}
private val max by lazy {
Math.max(
layout_message_attachment_options.width.toDouble(),
layout_message_attachment_options.height.toDouble()
).toFloat()
}
private val centerX by lazy { recycler_view.right }
private val centerY by lazy { recycler_view.bottom }
private val handler = Handler()
......@@ -97,6 +152,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
setHasOptionsMenu(true)
val bundle = arguments
if (bundle != null) {
......@@ -106,10 +162,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
isChatRoomReadOnly = bundle.getBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY)
isSubscribed = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED)
chatRoomLastSeen = bundle.getLong(BUNDLE_CHAT_ROOM_LAST_SEEN)
isChatRoomCreator = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_CREATOR)
chatRoomMessage = bundle.getString(BUNDLE_CHAT_ROOM_MESSAGE)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
setHasOptionsMenu(true)
}
override fun onCreateView(
......@@ -124,11 +181,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
super.onViewCreated(view, savedInstanceState)
setupToolbar(chatRoomName)
presenter.loadMessages(chatRoomId, chatRoomType)
presenter.setupChatRoom(chatRoomId, chatRoomName, chatRoomType, chatRoomMessage)
presenter.loadChatRooms()
setupRecyclerView()
setupFab()
setupMessageComposer()
setupSuggestionsView()
setupActionSnackbar()
activity?.apply {
......@@ -146,9 +202,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
recycler_view.removeOnScrollListener(onScrollListener)
recycler_view.removeOnLayoutChangeListener(layoutChangeListener)
presenter.unsubscribeMessages(chatRoomId)
presenter.disconnect()
handler.removeCallbacksAndMessages(null)
unsubscribeTextMessage()
unsubscribeComposeTextMessage()
// Hides the keyboard (if it's opened) before going to any view.
activity?.apply {
......@@ -168,6 +224,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.chatroom_actions, menu)
menu.findItem(R.id.action_members_list)?.isVisible = !isBroadcastChannel
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
......@@ -176,7 +233,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
presenter.toMembersList(chatRoomId, chatRoomType)
}
R.id.action_pinned_messages -> {
presenter.toPinnedMessageList(chatRoomId,chatRoomType)
presenter.toPinnedMessageList(chatRoomId, chatRoomType)
}
R.id.action_favorite_messages -> {
presenter.toFavoriteMessageList(chatRoomId, chatRoomType)
}
}
return true
......@@ -206,8 +266,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter,
reactionListener = this@ChatRoomFragment)
adapter = ChatRoomAdapter(
chatRoomType, chatRoomName, presenter,
reactionListener = this@ChatRoomFragment
)
recycler_view.adapter = adapter
if (dataSet.size >= 30) {
recycler_view.addOnScrollListener(endlessRecyclerViewScrollListener)
......@@ -227,19 +289,32 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean) {
// 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) {
if (size == 0){
if (size == 0) {
image_chat_icon.setVisible(true)
text_chat_title.setVisible(true)
text_chat_description.setVisible(true)
}else{
} else {
image_chat_icon.setVisible(false)
text_chat_title.setVisible(false)
text_chat_description.setVisible(false)
}
}
private val layoutChangeListener = View.OnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
private val layoutChangeListener =
View.OnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
val y = oldBottom - bottom
if (Math.abs(y) > 0 && isAdded) {
// if y is positive the keyboard is up else it's down
......@@ -287,7 +362,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (!recyclerView.canScrollVertically(1)) {
button_fab.hide()
} else {
if (dy < 0 && !button_fab.isVisible()) {
if (dy < 0 && !button_fab.isVisible) {
button_fab.show()
}
}
......@@ -306,6 +381,37 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun showTypingStatus(usernameList: ArrayList<String>) {
ui {
when (usernameList.size) {
1 -> {
text_typing_status.text =
SpannableStringBuilder()
.bold { append(usernameList[0]) }
.append(getString(R.string.msg_is_typing))
}
2 -> {
text_typing_status.text =
SpannableStringBuilder()
.bold { append(usernameList[0]) }
.append(getString(R.string.msg_and))
.bold { append(usernameList[1]) }
.append(getString(R.string.msg_are_typing))
}
else -> {
text_typing_status.text = getString(R.string.msg_several_users_are_typing)
}
}
text_typing_status.isVisible = true
}
}
override fun hideTypingStatusView() {
ui {
text_typing_status.isVisible = false
}
}
override fun uploadFile(uri: Uri) {
// TODO Just leaving a blank message that comes with the file for now. In the future lets add the possibility to add a message with the file to be uploaded.
presenter.uploadFile(chatRoomId, uri, "")
......@@ -363,19 +469,17 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun showReplyingAction(username: String, replyMarkdown: String, quotedMessage: String) {
override fun showReplyingAction(
username: String,
replyMarkdown: String,
quotedMessage: String
) {
ui {
citation = replyMarkdown
actionSnackbar.title = username
actionSnackbar.text = quotedMessage
actionSnackbar.show()
KeyboardHelper.showSoftKeyboard(text_message)
if (!recycler_view.isAtBottom()) {
if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
}
}
}
}
......@@ -508,21 +612,26 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
connection_status_text.text = getString(R.string.status_connected)
handler.postDelayed(dismissStatus, 2000)
}
is State.Disconnected -> connection_status_text.text = getString(R.string.status_disconnected)
is State.Connecting -> connection_status_text.text = getString(R.string.status_connecting)
is State.Authenticating -> connection_status_text.text = getString(R.string.status_authenticating)
is State.Disconnecting -> connection_status_text.text = getString(R.string.status_disconnecting)
is State.Waiting -> connection_status_text.text = getString(R.string.status_waiting, state.seconds)
is State.Disconnected -> connection_status_text.text =
getString(R.string.status_disconnected)
is State.Connecting -> connection_status_text.text =
getString(R.string.status_connecting)
is State.Authenticating -> connection_status_text.text =
getString(R.string.status_authenticating)
is State.Disconnecting -> connection_status_text.text =
getString(R.string.status_disconnecting)
is State.Waiting -> connection_status_text.text =
getString(R.string.status_waiting, state.seconds)
}
}
}
override fun onJoined() {
override fun onJoined(userCanPost: Boolean) {
ui {
input_container.setVisible(true)
button_join_chat.setVisible(false)
isSubscribed = true
setupMessageComposer()
setupMessageComposer(userCanPost)
}
}
......@@ -553,8 +662,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
private fun setupMessageComposer() {
if (isChatRoomReadOnly) {
private fun setupMessageComposer(canPost: Boolean) {
if (isChatRoomReadOnly && !canPost) {
text_room_is_read_only.setVisible(true)
input_container.setVisible(false)
} else if (!isSubscribed) {
......@@ -567,8 +676,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
button_show_attachment_options.alpha = 1f
button_show_attachment_options.setVisible(true)
subscribeTextMessage()
emojiKeyboardPopup = EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
subscribeComposeTextMessage()
emojiKeyboardPopup =
EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
emojiKeyboardPopup.listener = this
text_message.listener = object : ComposerEditText.ComposerEditTextListener {
override fun onKeyboardOpened() {
......@@ -658,7 +768,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
setReactionButtonIcon(R.drawable.ic_keyboard_black_24dp)
} 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()
setReactionButtonIcon(R.drawable.ic_reaction_24dp)
}
......@@ -672,18 +782,30 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
})
}
private fun subscribeTextMessage() {
val disposable = text_message.asObservable(0)
.subscribe({ t -> setupComposeMessageButtons(t) })
private fun subscribeComposeTextMessage() {
val editTextObservable = text_message.asObservable()
compositeDisposable.add(disposable)
compositeDisposable.addAll(
subscribeComposeButtons(editTextObservable),
subscribeComposeTypingStatus(editTextObservable)
)
}
private fun unsubscribeTextMessage() {
private fun unsubscribeComposeTextMessage() {
compositeDisposable.clear()
}
private fun setupComposeMessageButtons(charSequence: CharSequence) {
private fun subscribeComposeButtons(observable: Observable<CharSequence>): Disposable {
return observable.subscribe { t -> setupComposeButtons(t) }
}
private fun subscribeComposeTypingStatus(observable: Observable<CharSequence>): Disposable {
return observable.debounce(300, TimeUnit.MILLISECONDS)
.skip(1)
.subscribe { t -> sendTypingStatus(t) }
}
private fun setupComposeButtons(charSequence: CharSequence) {
if (charSequence.isNotEmpty() && playComposeMessageButtonsAnimation) {
button_show_attachment_options.fadeOut(1F, 0F, 120)
button_send.fadeIn(0F, 1F, 120)
......@@ -697,6 +819,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
private fun sendTypingStatus(charSequence: CharSequence) {
if (charSequence.isNotBlank()) {
presenter.sendTyping()
} else {
presenter.sendNotTyping()
}
}
private fun showAttachmentOptions() {
view_dim.setVisible(true)
......
......@@ -7,8 +7,10 @@ import chat.rocket.android.util.extensions.setVisible
/**
* An adapter for bottomsheet menu that lists all the actions that could be taken over a chat message.
*/
class ActionListAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuItem.OnMenuItemClickListener) :
ListBottomSheetAdapter(menuItems = menuItems, callback = callback) {
class ActionListAdapter(
menuItems: List<MenuItem> = emptyList(),
callback: MenuItem.OnMenuItemClickListener
) : ListBottomSheetAdapter(menuItems = menuItems, callback = callback) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = menuItems[position]
......@@ -25,7 +27,11 @@ class ActionListAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuI
callback?.onMenuItemClick(item)
}
val deleteTextColor = holder.itemView.context.resources.getColor(R.color.red)
val color = if (item.itemId == R.id.action_menu_msg_delete) deleteTextColor else textColors.get(item.itemId)
val color = if (item.itemId == R.id.action_message_delete) {
deleteTextColor
} else {
textColors.get(item.itemId)
}
holder.textTitle.setTextColor(color)
}
}
\ No newline at end of file
......@@ -24,7 +24,8 @@ interface BaseViewModel<out T> {
MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7),
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
import androidx.core.text.color
import androidx.core.text.scale
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.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.isNotNullNorEmpty
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
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.url.Url
import kotlinx.coroutines.experimental.CommonPool
......@@ -34,6 +50,8 @@ import javax.inject.Inject
class ViewModelMapper @Inject constructor(
private val context: Context,
private val parser: MessageParser,
private val roomsInteractor: ChatRoomsInteractor,
private val messageHelper: MessageHelper,
tokenRepository: TokenRepository,
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor,
......@@ -47,21 +65,24 @@ class ViewModelMapper @Inject constructor(
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
private val secondaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
suspend fun map(message: Message): List<BaseViewModel<*>> {
return translate(message)
suspend fun map(message: Message, roomViewModel: RoomViewModel = RoomViewModel(
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)
messages.forEach {
list.addAll(translate(it))
list.addAll(translate(it, roomViewModel))
}
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<*>>()
message.urls?.forEach {
......@@ -86,9 +107,40 @@ class ViewModelMapper @Inject constructor(
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
}
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<*>? {
if (url.ignoreParse || url.meta == null) return null
......@@ -322,6 +374,10 @@ class ViewModelMapper @Inject constructor(
is MessageType.RoomNameChanged -> context.getString(R.string.message_room_name_changed, message.message, message.sender?.username)
is MessageType.UserRemoved -> context.getString(R.string.message_user_removed_by, message.message, message.sender?.username)
is MessageType.MessagePinned -> context.getString(R.string.message_pinned)
is MessageType.UserMuted -> context.getString(R.string.message_muted, message.message, message.sender?.username)
is MessageType.UserUnMuted -> context.getString(R.string.message_unmuted, message.message, message.sender?.username)
is MessageType.SubscriptionRoleAdded -> context.getString(R.string.message_role_add, message.message, message.role, message.sender?.username)
is MessageType.SubscriptionRoleRemoved -> context.getString(R.string.message_role_removed, message.message, message.role, message.sender?.username)
else -> {
throw InvalidParameterException("Invalid message type: ${message.type}")
}
......
package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants
import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository
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.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.chatRooms
......@@ -23,12 +37,18 @@ import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.realtime.socket.model.StreamMessage
import chat.rocket.core.internal.realtime.socket.model.Type
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.permissions
import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.model.ChatRoom
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.async
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.reflect.KProperty1
......@@ -38,13 +58,16 @@ class ChatRoomsPresenter @Inject constructor(
private val strategy: CancelStrategy,
private val navigator: MainNavigator,
private val serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val chatRoomsInteractor: ChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val saveActiveUsersInteractor: SaveActiveUsersInteractor,
private val getActiveUsersInteractor: GetActiveUsersInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val viewModelMapper: ViewModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor,
private val permissionsInteractor: PermissionsInteractor,
private val localRepository: LocalRepository,
private val userHelper: UserHelper,
settingsRepository: SettingsRepository,
factory: ConnectionManagerFactory
) {
......@@ -69,6 +92,8 @@ class ChatRoomsPresenter @Inject constructor(
refreshSettingsInteractor.refresh(currentServer)
}
view.updateChatRooms(getUserChatRooms())
val permissions = retryIO { client.permissions() }
permissionsInteractor.saveAll(permissions)
} catch (ex: RocketChatException) {
ex.message?.let {
view.showMessage(it)
......@@ -85,7 +110,8 @@ class ChatRoomsPresenter @Inject constructor(
}
fun loadChatRoom(chatRoom: ChatRoom) {
val roomName = if (chatRoom.type is RoomType.DirectMessage
val isDirectMessage = chatRoom.type is RoomType.DirectMessage
val roomName = if (isDirectMessage
&& chatRoom.fullName != null
&& settings.useRealName()) {
chatRoom.fullName!!
......@@ -93,10 +119,40 @@ class ChatRoomsPresenter @Inject constructor(
chatRoom.name
}
launchUI(strategy) {
val myself = getCurrentUser()
if (myself?.username == null) {
view.showMessage(R.string.msg_generic_error)
} else {
val isChatRoomOwner = chatRoom.user?.username == myself.username || isDirectMessage
navigator.toChatRoom(chatRoom.id, roomName,
chatRoom.type.toString(), chatRoom.readonly ?: false,
chatRoom.lastSeen ?: -1,
chatRoom.open)
chatRoom.open, isChatRoomOwner)
}
}
}
private suspend fun getCurrentUser(): User? {
userHelper.user()?.let {
return it
}
try {
val myself = retryIO { client.me() }
val user = User(
id = myself.id,
username = myself.username,
name = myself.name,
status = myself.status,
utcOffset = myself.utcOffset,
emails = null,
roles = myself.roles
)
localRepository.saveCurrentUser(url = currentServer, user = user)
} catch (ex: RocketChatException) {
Timber.e(ex)
}
return null
}
/**
......@@ -107,7 +163,7 @@ class ChatRoomsPresenter @Inject constructor(
val currentServer = serverInteractor.get()!!
launchUI(strategy) {
try {
val roomList = getChatRoomsInteractor.getAllByName(currentServer, name)
val roomList = chatRoomsInteractor.getAllByName(currentServer, name)
if (roomList.isEmpty()) {
val (users, rooms) = retryIO("spotlight($name)") {
client.spotlight(name)
......@@ -170,7 +226,8 @@ class ChatRoomsPresenter @Inject constructor(
userMentions = null,
groupMentions = 0L,
lastMessage = null,
client = client
client = client,
broadcast = false
)
}
}
......@@ -203,14 +260,15 @@ class ChatRoomsPresenter @Inject constructor(
userMentions = null,
groupMentions = 0L,
lastMessage = it.lastMessage,
client = client
client = client,
broadcast = it.broadcast
)
}
}
fun updateSortedChatRooms() {
launchUI(strategy) {
val roomList = getChatRoomsInteractor.getAll(currentServer)
val roomList = chatRoomsInteractor.getAll(currentServer)
view.updateChatRooms(sortRooms(roomList))
}
}
......@@ -294,7 +352,8 @@ class ChatRoomsPresenter @Inject constructor(
userMentions = it.userMentions,
groupMentions = it.groupMentions,
lastMessage = it.lastMessage,
client = client
client = client,
broadcast = it.broadcast
)
chatRoomsList.add(newRoom)
}
......@@ -409,13 +468,13 @@ class ChatRoomsPresenter @Inject constructor(
// Update a ChatRoom with a Room information
private fun updateRoom(room: Room) {
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 }
chatRoom?.apply {
val newRoom = ChatRoom(
id = room.id,
type = room.type,
user = room.user ?: user,
user = room.user,
status = getActiveUsersInteractor.getActiveUserByUsername(
currentServer,
room.name ?: name
......@@ -437,7 +496,8 @@ class ChatRoomsPresenter @Inject constructor(
userMentions = userMentions,
groupMentions = groupMentions,
lastMessage = room.lastMessage,
client = client
client = client,
broadcast = broadcast
)
removeRoom(room.id, chatRooms)
chatRooms.add(newRoom)
......@@ -448,13 +508,13 @@ class ChatRoomsPresenter @Inject constructor(
// Update a ChatRoom with a Subscription information
private fun updateSubscription(subscription: Subscription) {
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 }
chatRoom?.apply {
val newRoom = ChatRoom(
id = subscription.roomId,
type = subscription.type,
user = subscription.user ?: user,
user = user,
status = getActiveUsersInteractor.getActiveUserByUsername(
currentServer,
subscription.name
......@@ -476,7 +536,8 @@ class ChatRoomsPresenter @Inject constructor(
userMentions = subscription.userMentions,
groupMentions = subscription.groupMentions,
lastMessage = lastMessage,
client = client
client = client,
broadcast = broadcast
)
removeRoom(subscription.roomId, chatRooms)
chatRooms.add(newRoom)
......@@ -486,7 +547,7 @@ class ChatRoomsPresenter @Inject constructor(
private fun removeRoom(
id: String,
chatRooms: MutableList<ChatRoom> = getChatRoomsInteractor.getAll(currentServer).toMutableList()
chatRooms: MutableList<ChatRoom> = chatRoomsInteractor.getAll(currentServer).toMutableList()
) {
Timber.d("Removing ROOM: $id")
synchronized(this) {
......@@ -527,7 +588,7 @@ class ChatRoomsPresenter @Inject constructor(
val username = user_.username
val status = user_.status
if (username != null && status != null) {
getChatRoomsInteractor.getByName(currentServer, username)?.let {
chatRoomsInteractor.getByName(currentServer, username)?.let {
val newRoom = ChatRoom(
id = it.id,
type = it.type,
......@@ -550,13 +611,14 @@ class ChatRoomsPresenter @Inject constructor(
userMentions = it.userMentions,
groupMentions = it.groupMentions,
lastMessage = it.lastMessage,
client = client
client = client,
broadcast = it.broadcast
)
getChatRoomsInteractor.remove(currentServer, it)
getChatRoomsInteractor.add(currentServer, newRoom)
chatRoomsInteractor.remove(currentServer, it)
chatRoomsInteractor.add(currentServer, newRoom)
launchUI(strategy) {
view.updateChatRooms(sortRooms(getChatRoomsInteractor.getAll(currentServer)))
view.updateChatRooms(sortRooms(chatRoomsInteractor.getAll(currentServer)))
}
}
}
......@@ -566,7 +628,7 @@ class ChatRoomsPresenter @Inject constructor(
Timber.i("Updating ChatRooms")
launch(strategy.jobs) {
val chatRoomsWithPreview = getChatRoomsWithPreviews(
getChatRoomsInteractor.getAll(currentServer)
chatRoomsInteractor.getAll(currentServer)
)
val chatRoomsWithStatus = getChatRoomWithStatus(chatRoomsWithPreview)
view.updateChatRooms(chatRoomsWithStatus)
......
......@@ -25,10 +25,13 @@ import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class ChatRoomsAdapter(private val context: Context,
class ChatRoomsAdapter(
private val context: Context,
private val settings: PublicSettings,
private val localRepository: LocalRepository,
private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() {
private val listener: (ChatRoom) -> Unit
) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() {
var dataSet: MutableList<ChatRoom> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = ViewHolder(parent.inflate(R.layout.item_chat))
......
......@@ -14,6 +14,7 @@ import android.support.v7.widget.SearchView
import android.view.*
import android.widget.CheckBox
import android.widget.RadioGroup
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
......@@ -23,7 +24,6 @@ import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.showLastMessage
import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.common.model.RoomType
......@@ -37,11 +37,14 @@ import timber.log.Timber
import javax.inject.Inject
class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject lateinit var presenter: ChatRoomsPresenter
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var settingsRepository: SettingsRepository
@Inject lateinit var localRepository: LocalRepository
private lateinit var preferences: SharedPreferences
@Inject
lateinit var presenter: ChatRoomsPresenter
@Inject
lateinit var serverInteractor: GetCurrentServerInteractor
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var localRepository: LocalRepository
private var searchView: SearchView? = null
private val handler = Handler()
......@@ -56,7 +59,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
setHasOptionsMenu(true)
preferences = context?.getSharedPreferences("temp", Context.MODE_PRIVATE)!!
}
override fun onDestroy() {
......@@ -146,9 +148,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
return super.onOptionsItemSelected(item)
}
private fun invalidateQueryOnSearch(){
private fun invalidateQueryOnSearch() {
searchView?.let {
if (!searchView!!.isIconified){
if (!searchView!!.isIconified) {
queryChatRoomsByName(searchView!!.query.toString())
}
}
......@@ -163,7 +165,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
/*val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet))
}.await()*/
text_no_search.isVisible = newDataSet.isEmpty()
if (isActive) {
adapter.baseAdapter.updateRooms(newDataSet)
// TODO - fix crash to re-enable diff.dispatchUpdatesTo(adapter)
......@@ -179,7 +181,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
ui { text_no_data_to_display.setVisible(true) }
}
override fun showLoading(){
override fun showLoading() {
ui { view_loading.setVisible(true) }
}
......@@ -241,10 +243,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
recycler_view.itemAnimator = DefaultItemAnimator()
// TODO - use a ViewModel Mapper instead of using settings on the adapter
println(serverInteractor.get() + " -> ${settingsRepository.get(serverInteractor.get()!!).showLastMessage()}")
val baseAdapter = ChatRoomsAdapter(it,
settingsRepository.get(serverInteractor.get()!!), localRepository) {
chatRoom -> presenter.loadChatRoom(chatRoom)
settingsRepository.get(serverInteractor.get()!!), localRepository) { chatRoom ->
presenter.loadChatRoom(chatRoom)
}
sectionedAdapter = SimpleSectionedRecyclerViewAdapter(it,
......
......@@ -10,6 +10,7 @@ import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider
import chat.rocket.android.chatroom.di.ChatRoomModule
import chat.rocket.android.chatroom.di.FavoriteMessagesFragmentProvider
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
......@@ -47,10 +48,15 @@ abstract class ActivityBuilder {
abstract fun bindMainActivity(): MainActivity
@PerActivity
@ContributesAndroidInjector(modules = [ChatRoomModule::class,
@ContributesAndroidInjector(
modules = [
ChatRoomModule::class,
ChatRoomFragmentProvider::class,
MembersFragmentProvider::class,
PinnedMessagesFragmentProvider::class])
PinnedMessagesFragmentProvider::class,
FavoriteMessagesFragmentProvider::class
]
)
abstract fun bindChatRoomActivity(): ChatRoomActivity
@PerActivity
......
......@@ -14,12 +14,10 @@ import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.dagger.qualifier.ForMessages
import chat.rocket.android.helper.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.infrastructure.SharedPreferencesLocalRepository
import chat.rocket.android.push.GroupedPush
import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.*
......@@ -43,7 +41,6 @@ import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import ru.noties.markwon.SpannableConfiguration
......@@ -119,24 +116,8 @@ class AppModule {
}
@Provides
@ForFresco
@Singleton
fun provideFrescoAuthInterceptor(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
return FrescoAuthInterceptor(tokenRepository, currentServerInteractor)
}
@Provides
@ForFresco
@Singleton
fun provideFrescoOkHttpClient(okHttpClient: OkHttpClient, @ForFresco authInterceptor: Interceptor): OkHttpClient {
return okHttpClient.newBuilder().apply {
//addInterceptor(authInterceptor)
}.build()
}
@Provides
@Singleton
fun provideImagePipelineConfig(context: Context, @ForFresco okHttpClient: OkHttpClient): ImagePipelineConfig {
fun provideImagePipelineConfig(context: Context, okHttpClient: OkHttpClient): ImagePipelineConfig {
val listeners = setOf(RequestLoggingListener())
return OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient)
......@@ -177,8 +158,8 @@ class AppModule {
@Provides
@Singleton
fun provideLocalRepository(prefs: SharedPreferences): LocalRepository {
return SharedPrefsLocalRepository(prefs)
fun provideLocalRepository(prefs: SharedPreferences, moshi: Moshi): LocalRepository {
return SharedPreferencesLocalRepository(prefs, moshi)
}
@Provides
......@@ -193,6 +174,12 @@ class AppModule {
return SharedPreferencesSettingsRepository(localRepository)
}
@Provides
@Singleton
fun providePermissionsRepository(localRepository: LocalRepository, moshi: Moshi): PermissionsRepository {
return SharedPreferencesPermissionsRepository(localRepository, moshi)
}
@Provides
@Singleton
fun provideRoomRepository(): RoomRepository {
......@@ -258,7 +245,7 @@ class AppModule {
@Provides
@Singleton
fun provideConfiguration(context: Application, client: OkHttpClient): SpannableConfiguration {
fun provideConfiguration(context: Application): SpannableConfiguration {
val res = context.resources
return SpannableConfiguration.builder(context)
.theme(SpannableTheme.builder()
......@@ -273,12 +260,6 @@ class AppModule {
return MessageParser(context, configuration, settingsInteractor.get(url))
}
@Provides
@Singleton
fun providePermissionInteractor(settingsRepository: SettingsRepository, serverRepository: CurrentServerRepository): GetPermissionsInteractor {
return GetPermissionsInteractor(settingsRepository, serverRepository)
}
@Provides
@Singleton
fun provideAccountsRepository(preferences: SharedPreferences, moshi: Moshi): AccountsRepository =
......
......@@ -3,7 +3,21 @@ package chat.rocket.android.dagger.module
import android.content.Context
import android.content.SharedPreferences
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.infrastructure.SharedPreferencesLocalRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.internal.AttachmentAdapterFactory
import chat.rocket.core.internal.ReactionsAdapter
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
......@@ -11,14 +25,53 @@ import javax.inject.Singleton
@Module
class LocalModule {
@Provides
@Singleton
fun providePlatformLogger(): PlatformLogger {
return TimberLogger
}
@Provides
@Singleton
fun provideCurrentServerRepository(prefs: SharedPreferences): CurrentServerRepository {
return SharedPrefsCurrentServerRepository(prefs)
}
@Provides
@Singleton
fun provideMoshi(
logger: PlatformLogger,
currentServerInteractor: GetCurrentServerInteractor
): Moshi {
val url = currentServerInteractor.get() ?: ""
return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE)
.add(AttachmentAdapterFactory(Logger(logger, url)))
.add(
java.lang.Long::class.java,
ISO8601Date::class.java,
TimestampAdapter(CalendarISO8601Converter())
)
.add(
Long::class.java,
ISO8601Date::class.java,
TimestampAdapter(CalendarISO8601Converter())
)
.add(ReactionsAdapter())
.build()
}
@Provides
fun provideSharedPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
}
@Provides
@Singleton
fun provideLocalRepository(prefs: SharedPreferences): LocalRepository {
return SharedPrefsLocalRepository(prefs)
fun provideLocalRepository(sharedPreferences: SharedPreferences, moshi: Moshi): LocalRepository {
return SharedPreferencesLocalRepository(sharedPreferences, moshi)
}
}
\ No newline at end of file
package chat.rocket.android.dagger.qualifier
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ForFresco
\ No newline at end of file
package chat.rocket.android.chatroom.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesView
import chat.rocket.android.favoritemessages.ui.FavoriteMessagesFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class FavoriteMessagesFragmentModule {
@Provides
fun provideLifecycleOwner(frag: FavoriteMessagesFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
@Provides
fun provideFavoriteMessagesView(frag: FavoriteMessagesFragment): FavoriteMessagesView {
return frag
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.di
import chat.rocket.android.favoritemessages.ui.FavoriteMessagesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class FavoriteMessagesFragmentProvider {
@ContributesAndroidInjector(modules = [FavoriteMessagesFragmentModule::class])
abstract fun provideFavoriteMessageFragment(): FavoriteMessagesFragment
}
\ No newline at end of file
package chat.rocket.android.favoritemessages.presentation
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getFavoriteMessages
import timber.log.Timber
import javax.inject.Inject
class FavoriteMessagesPresenter @Inject constructor(
private val view: FavoriteMessagesView,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: ChatRoomsInteractor,
private val mapper: ViewModelMapper,
factory: RocketChatClientFactory
) {
private val client = factory.create(serverInteractor.get()!!)
private var offset: Int = 0
/**
* Loads all favorite messages for room. the given room id.
*
* @param roomId The id of the room to get its favorite messages.
*/
fun loadFavoriteMessages(roomId: String) {
launchUI(strategy) {
try {
val serverUrl = serverInteractor.get()!!
val chatRoom = roomsInteractor.getById(serverUrl, roomId)
chatRoom?.let { room ->
view.showLoading()
val favoriteMessages =
client.getFavoriteMessages(roomId, room.type, offset)
offset = favoriteMessages.offset.toInt()
val messageList = mapper.map(favoriteMessages.result)
view.showFavoriteMessages(messageList)
view.hideLoading()
}.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server.")
}
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
}
\ No newline at end of file
package chat.rocket.android.favoritemessages.presentation
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface FavoriteMessagesView : MessageView, LoadingView {
/**
* Shows the list of favorite messages for the current room.
*
* @param favoriteMessages The list of favorite messages to show.
*/
fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>)
}
\ No newline at end of file
package chat.rocket.android.favoritemessages.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesPresenter
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesView
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_favorite_messages.*
import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment {
return FavoriteMessagesFragment().apply {
arguments = Bundle(1).apply {
putString(INTENT_CHAT_ROOM_ID, chatRoomId)
putString(INTENT_CHAT_ROOM_TYPE, chatRoomType)
}
}
}
private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
private lateinit var adapter: ChatRoomAdapter
@Inject
lateinit var presenter: FavoriteMessagesPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(INTENT_CHAT_ROOM_ID)
chatRoomType = bundle.getString(INTENT_CHAT_ROOM_TYPE)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_favorite_messages)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
presenter.loadFavoriteMessages(chatRoomId)
}
override fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>) {
ui {
if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, "", null, false)
recycler_view.adapter = adapter
val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view.layoutManager = linearLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator()
if (favoriteMessages.size > 10) {
recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView?
) {
presenter.loadFavoriteMessages(chatRoomId)
}
})
}
no_messages_view.isVisible = favoriteMessages.isEmpty()
}
adapter.appendData(favoriteMessages)
}
}
override fun showMessage(resId: Int) {
ui { showToast(resId) }
}
override fun showMessage(message: String) {
ui { showToast(message) }
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showLoading() {
ui { view_loading.isVisible = true }
}
override fun hideLoading() {
ui { view_loading.isVisible = false }
}
private fun setupToolbar() {
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_favorite_messages))
}
}
\ No newline at end of file
package chat.rocket.android.helper
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.TokenRepository
import okhttp3.Interceptor
import okhttp3.Response
class FrescoAuthInterceptor(
private val tokenRepository: TokenRepository,
private val currentServerInteractor: GetCurrentServerInteractor
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
currentServerInteractor.get()?.let { serverUrl ->
val token = tokenRepository.get(serverUrl)
return@let token?.let {
val url = request.url().newBuilder().apply {
addQueryParameter("rc_uid", token.userId)
addQueryParameter("rc_token", token.authToken)
}.build()
request = request.newBuilder().apply {
url(url)
}.build()
}
}
return chain.proceed(request)
}
}
\ No newline at end of file
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
package chat.rocket.android.helper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.useRealName
import chat.rocket.common.model.User
import javax.inject.Inject
class UserHelper @Inject constructor(
private val localRepository: LocalRepository,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
settingsRepository: SettingsRepository
) {
private val settings: PublicSettings = settingsRepository.get(getCurrentServerInteractor.get()!!)
/**
* Return the display name for the given [user].
* If setting 'Use_Real_Name' is true then the real name will be given, or else
* the username without the '@' is yielded. The fallback for any case is the username, which
* could be null.
*/
fun displayName(user: User): String? {
return if (settings.useRealName()) user.name ?: user.username else user.username
}
/**
* Return current logged user's display name.
*
* @see displayName
*/
fun displayName(): String? {
user()?.let {
return displayName(it)
}
return null
}
/**
* Return current logged [User].
*/
fun user(): User? {
return localRepository.getCurrentUser(serverUrl())
}
/**
* Return the username for the current logged [User].
*/
fun username(): String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY, null)
/**
* Whether current [User] is admin on the current server.
*/
fun isAdmin(): Boolean {
return user()?.roles?.find { it.equals("admin", ignoreCase = true) } != null
}
private fun serverUrl(): String {
return getCurrentServerInteractor.get()!!
}
}
\ No newline at end of file
package chat.rocket.android.infrastructure
import chat.rocket.common.model.User
interface LocalRepository {
fun save(key: String, value: String?)
......@@ -14,12 +16,16 @@ interface LocalRepository {
fun getLong(key: String, defValue: Long = -1L): Long
fun clear(key: String)
fun clearAllFromServer(server: String)
fun getCurrentUser(url: String): User?
fun saveCurrentUser(url: String, user: User)
companion object {
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
const val MIGRATION_FINISHED_KEY = "MIGRATION_FINISHED_KEY"
const val TOKEN_KEY = "token_"
const val SETTINGS_KEY = "settings_"
const val PERMISSIONS_KEY = "permissions_"
const val USER_KEY = "user_"
const val CURRENT_USERNAME_KEY = "username_"
}
}
......
......@@ -2,8 +2,26 @@ package chat.rocket.android.infrastructure
import android.content.SharedPreferences
import androidx.core.content.edit
import chat.rocket.common.model.User
import com.squareup.moshi.Moshi
class SharedPreferencesLocalRepository(
private val preferences: SharedPreferences,
moshi: Moshi
) : LocalRepository {
private val userAdapter = moshi.adapter(User::class.java)
override fun getCurrentUser(url: String): User? {
return get("${url}_${LocalRepository.USER_KEY}", null)?.let {
userAdapter.fromJson(it)
}
}
override fun saveCurrentUser(url: String, user: User) {
save("${url}_${LocalRepository.USER_KEY}", userAdapter.toJson(user))
}
class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : LocalRepository {
override fun getBoolean(key: String, defValue: Boolean) = preferences.getBoolean(key, defValue)
override fun getFloat(key: String, defValue: Float) = preferences.getFloat(key, defValue)
......
package chat.rocket.android.main.presentation
import android.content.Context
import chat.rocket.android.R
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.chatroom.ui.chatRoomIntent
......@@ -36,9 +35,10 @@ class MainNavigator(internal val activity: MainActivity) {
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean) {
isChatRoomSubscribed: Boolean,
isChatRoomCreator: Boolean) {
activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed))
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomCreator))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
......
......@@ -2,7 +2,6 @@ package chat.rocket.android.main.viewmodel
import chat.rocket.common.model.UserStatus
data class NavHeaderViewModel(
val userDisplayName: String?,
val userStatus: UserStatus?,
......
......@@ -6,7 +6,6 @@ import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
......@@ -38,9 +37,12 @@ private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
class MembersFragment : Fragment(), MembersView {
@Inject lateinit var presenter: MembersPresenter
private val adapter: MembersAdapter = MembersAdapter { memberViewModel -> presenter.toMemberDetails(memberViewModel) }
private val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
@Inject
lateinit var presenter: MembersPresenter
private val adapter: MembersAdapter =
MembersAdapter { memberViewModel -> presenter.toMemberDetails(memberViewModel) }
private val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
......@@ -58,13 +60,15 @@ class MembersFragment : Fragment(), MembersView {
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_members)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_members)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as AppCompatActivity).supportActionBar?.title = ""
setupRecyclerView()
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType)
}
......@@ -75,8 +79,13 @@ class MembersFragment : Fragment(), MembersView {
if (adapter.itemCount == 0) {
adapter.prependData(dataSet)
if (dataSet.size >= 59) { // TODO Check why the API retorns the specified count -1
recycler_view.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView?
) {
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType, page * 60L)
}
})
......@@ -84,18 +93,7 @@ class MembersFragment : Fragment(), MembersView {
} else {
adapter.appendData(dataSet)
}
if (it is ChatRoomActivity) {
it.showRoomTypeIcon(false)
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
(activity as ChatRoomActivity).showRoomTypeIcon(true)
return super.onOptionsItemSelected(item)
}
return super.onOptionsItemSelected(item)
}
override fun showLoading() {
......@@ -129,6 +127,8 @@ class MembersFragment : Fragment(), MembersView {
}
private fun setupToolbar(totalMembers: Long) {
(activity as ChatRoomActivity?)?.setupToolbarTitle(getString(R.string.title_members, totalMembers))
(activity as ChatRoomActivity?)?.setupToolbarTitle(
getString(R.string.title_members, totalMembers)
)
}
}
\ No newline at end of file
......@@ -2,13 +2,13 @@ package chat.rocket.android.pinnedmessages.presentation
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
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.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getRoomPinnedMessages
import chat.rocket.core.internal.rest.getPinnedMessages
import chat.rocket.core.model.isSystemMessage
import timber.log.Timber
import javax.inject.Inject
......@@ -17,7 +17,7 @@ class PinnedMessagesPresenter @Inject constructor(
private val view: PinnedMessagesView,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor,
private val roomsInteractor: ChatRoomsInteractor,
private val mapper: ViewModelMapper,
factory: RocketChatClientFactory
) {
......@@ -37,7 +37,7 @@ class PinnedMessagesPresenter @Inject constructor(
chatRoom?.let { room ->
view.showLoading()
val pinnedMessages =
client.getRoomPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
client.getPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
pinnedMessagesListOffset = pinnedMessages.offset.toInt()
val messageList = mapper.map(pinnedMessages.result.filterNot { it.isSystemMessage() })
view.showPinnedMessages(messageList)
......
......@@ -2,13 +2,13 @@ package chat.rocket.android.pinnedmessages.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.ui.ChatRoomActivity
......@@ -17,14 +17,13 @@ import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.pinnedmessages.presentation.PinnedMessagesPresenter
import chat.rocket.android.pinnedmessages.presentation.PinnedMessagesView
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_pinned_messages.*
import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String) : Fragment {
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment {
return PinnedMessagesFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
......@@ -49,15 +48,19 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null){
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
}else{
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_pinned_messages)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_pinned_messages)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -69,21 +72,27 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
ui {
if (recycler_view_pinned.adapter == null){
adapter = ChatRoomAdapter(chatRoomType,"",null,false)
if (recycler_view_pinned.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, "", null, false)
recycler_view_pinned.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL,false)
val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view_pinned.layoutManager = linearLayoutManager
recycler_view_pinned.itemAnimator = DefaultItemAnimator()
if (pinnedMessages.size > 10){
recycler_view_pinned.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager){
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
if (pinnedMessages.size > 10) {
recycler_view_pinned.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView?
) {
presenter.loadPinnedMessages(chatRoomId)
}
})
}
togglePinView(pinnedMessages.size)
pin_view.isVisible = pinnedMessages.isEmpty()
}
adapter.appendData(pinnedMessages)
}
......@@ -104,22 +113,14 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showLoading() {
ui{ view_loading.setVisible(true) }
ui { view_loading.isVisible = true }
}
override fun hideLoading() {
ui { view_loading.setVisible(false) }
ui { view_loading.isVisible = false }
}
private fun setupToolbar() {
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_pinned_messages))
}
private fun togglePinView(size : Int){
if (size == 0){
pin_view.setVisible(true)
}else{
pin_view.setVisible(false)
}
}
}
\ No newline at end of file
......@@ -291,7 +291,7 @@ class PushManager @Inject constructor(
.setLabel(replyTextHint)
.build()
val pendingIntent = getReplyPendingIntent(pushMessage)
val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply_black_24px, replyTextHint, pendingIntent)
val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_action_message_reply_24dp, replyTextHint, pendingIntent)
.addRemoteInput(replyRemoteInput)
.setAllowGeneratedReplies(true)
.build()
......
......@@ -5,7 +5,7 @@ import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
class ChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
/**
* Get all [ChatRoom].
......@@ -41,8 +41,7 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo
* @return The [ChatRoom] object or null if we couldn't find any.
*/
suspend fun getById(serverUrl: String, roomId: String): ChatRoom? = withContext(CommonPool) {
val allChatRooms = repository.get(serverUrl)
return@withContext allChatRooms.first {
return@withContext repository.get(serverUrl).find {
it.id == roomId
}
}
......@@ -55,7 +54,7 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo
* @return The [ChatRoom] object or null if we couldn't find any.
*/
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 }
}
/**
......
package chat.rocket.android.server.domain
import javax.inject.Inject
class GetPermissionsInteractor @Inject constructor(private val settingsRepository: SettingsRepository,
private val currentServerRepository: CurrentServerRepository) {
private fun publicSettings(): PublicSettings? = settingsRepository.get(currentServerRepository.get()!!)
/**
* Check whether user is allowed to delete a message.
*/
fun allowedMessageDeleting() = publicSettings()?.allowedMessageDeleting() ?: false
/**
* Checks whether user is allowed to edit a message.
*/
fun allowedMessageEditing() = publicSettings()?.allowedMessageEditing() ?: false
/**
* Checks whether user is allowed to pin a message to a channel.
*/
fun allowedMessagePinning() = publicSettings()?.allowedMessagePinning() ?: false
/**
* Checks whether should show deleted message status.
*/
fun showDeletedStatus() = publicSettings()?.showDeletedStatus() ?: false
/**
* Checks whether should show edited message status.
*/
fun showEditedStatus() = publicSettings()?.showEditedStatus() ?: false
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.core.model.Permission
import javax.inject.Inject
// Creating rooms
const val CREATE_PUBLIC_CHANNELS = "create-c"
const val CREATE_DIRECT_MESSAGES = "create-d"
const val CREATE_PRIVATE_CHANNELS = "create-p"
// Messages
const val DELETE_MESSAGE = "delete-message"
const val FORCE_DELETE_MESSAGE = "force-delete-message"
const val EDIT_MESSAGE = "edit-message"
const val PIN_MESSAGE = "pin-message"
const val POST_READONLY = "post-readonly"
class PermissionsInteractor @Inject constructor(
private val settingsRepository: SettingsRepository,
private val permissionsRepository: PermissionsRepository,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val userHelper: UserHelper
) {
private fun publicSettings(): PublicSettings? = settingsRepository.get(currentServerUrl()!!)
fun saveAll(permissions: List<Permission>) {
val url = currentServerUrl()!!
permissions.forEach { permissionsRepository.save(url, it) }
}
/**
* Check whether the user is allowed to delete a message.
*/
fun allowedMessageDeleting() = publicSettings()?.allowedMessageDeleting() ?: false
/**
* Checks whether the user is allowed to edit a message.
*/
fun allowedMessageEditing() = publicSettings()?.allowedMessageEditing() ?: false
/**
* Checks whether the user is allowed to pin a message to a channel.
*/
fun allowedMessagePinning() = publicSettings()?.allowedMessagePinning() ?: false
/**
* Checks whether the user is allowed to star a message.
*/
fun allowedMessageStarring() = publicSettings()?.allowedMessageStarring() ?: false
/**
* Checks whether should show deleted message status.
*/
fun showDeletedStatus() = publicSettings()?.showDeletedStatus() ?: false
/**
* Checks whether should show edited message status.
*/
fun showEditedStatus() = publicSettings()?.showEditedStatus() ?: false
fun canPostToReadOnlyChannels(): Boolean {
val url = getCurrentServerInteractor.get()!!
val currentUserRoles = userHelper.user()?.roles
return permissionsRepository.get(url, POST_READONLY)?.let { permission ->
currentUserRoles?.isNotEmpty() == true && permission.roles.any {
currentUserRoles.contains(it)
}
} == true || userHelper.isAdmin()
}
private fun currentServerUrl(): String? {
return getCurrentServerInteractor.get()
}
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.core.model.Permission
interface PermissionsRepository {
/**
* Store [permission] locally.
*
* @param url The server url from where we're interest to store the permission.
* @param permission The permission to store.
*/
fun save(url: String, permission: Permission)
/**
* Get permission given by the [permissionId] and for the server [url].
*
* @param url The server url from where we're interested on getting the permissions.
* @param permissionId the id of the permission to get.
*
* @return The interested [Permission] or null if not found.
*/
fun get(url: String, permissionId: String): Permission?
}
\ No newline at end of file
......@@ -24,7 +24,7 @@ class RefreshSettingsInteractor @Inject constructor(
FAVORITE_ROOMS, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE, UPLOAD_WHITELIST_MIMETYPES,
HIDE_USER_JOIN, HIDE_USER_LEAVE,
HIDE_TYPE_AU, HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ALLOW_MESSAGE_DELETING,
ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS,
ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, ALLOW_MESSAGE_STARRING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS,
WIDE_TILE_310, STORE_LAST_MESSAGE)
suspend fun refresh(server: String) {
......
......@@ -29,7 +29,8 @@ class SaveActiveUsersInteractor @Inject constructor(
username = user.username ?: it.username,
status = user.status ?: it.status,
emails = user.emails ?: it.emails,
utcOffset = user.utcOffset ?: it.utcOffset
utcOffset = user.utcOffset ?: it.utcOffset,
roles = user.roles ?: it.roles
)
val activeUserList: MutableList<User> =
......
......@@ -49,6 +49,7 @@ const val ALLOW_MESSAGE_EDITING = "Message_AllowEditing"
const val SHOW_DELETED_STATUS = "Message_ShowDeletedStatus"
const val SHOW_EDITED_STATUS = "Message_ShowEditedStatus"
const val ALLOW_MESSAGE_PINNING = "Message_AllowPinning"
const val ALLOW_MESSAGE_STARRING = "Message_AllowStarring"
const val STORE_LAST_MESSAGE = "Store_Last_Message"
/*
......@@ -83,6 +84,7 @@ fun PublicSettings.wideTile(): String? = this[WIDE_TILE_310]?.value as String?
fun PublicSettings.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
fun PublicSettings.showEditedStatus(): Boolean = this[SHOW_EDITED_STATUS]?.value == true
fun PublicSettings.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING]?.value == true
fun PublicSettings.allowedMessageStarring(): Boolean = this[ALLOW_MESSAGE_STARRING]?.value == true
fun PublicSettings.allowedMessageEditing(): Boolean = this[ALLOW_MESSAGE_EDITING]?.value == true
fun PublicSettings.allowedMessageDeleting(): Boolean = this[ALLOW_MESSAGE_DELETING]?.value == true
......
......@@ -110,7 +110,6 @@ class ConnectionManager(internal val client: RocketChatClient) {
Timber.d("Received new Message for room ${message.roomId}")
val channel = roomMessagesChannels[message.roomId]
channel?.send(message)
}
}
......@@ -131,6 +130,7 @@ class ConnectionManager(internal val client: RocketChatClient) {
}
}
}
client.connect()
// Broadcast initial state...
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.PermissionsRepository
import chat.rocket.core.model.Permission
import com.squareup.moshi.Moshi
class SharedPreferencesPermissionsRepository(
private val localRepository: LocalRepository,
moshi: Moshi
) : PermissionsRepository {
private val adapter = moshi.adapter(Permission::class.java)
override fun save(url: String, permission: Permission) {
localRepository.save(getPermissionKey(url, permission.id), adapter.toJson(permission))
}
override fun get(url: String, permissionId: String): Permission? {
return localRepository.get(getPermissionKey(url, permissionId))?.let {
adapter.fromJson(it)
}
}
// Create a key following the pattern: settings_[url]_[permission id]
// eg.: 'settings_https://open.rocket.chat_create-p'
private fun getPermissionKey(url: String, permissionId: String): String {
return "${LocalRepository.PERMISSIONS_KEY}${url}_$permissionId"
}
}
\ No newline at end of file
......@@ -6,7 +6,9 @@ import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.core.internal.SettingsAdapter
class SharedPreferencesSettingsRepository(private val localRepository: LocalRepository) : SettingsRepository {
class SharedPreferencesSettingsRepository(
private val localRepository: LocalRepository
) : SettingsRepository {
private val adapter = SettingsAdapter().lenient()
......
......@@ -6,9 +6,8 @@ import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
fun EditText.asObservable(debounceTimeout: Long = 100): Observable<CharSequence> {
fun EditText.asObservable(): Observable<CharSequence> {
return RxTextView.textChanges(this)
.debounce(debounceTimeout, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
}
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"
android:fillColor="@color/actionMenuColor"/>
android:fillColor="@color/actionMenuColor"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"
android:fillColor="#FF0000"/>
android:fillColor="#FF0000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"
android:fillColor="@color/actionMenuColor"/>
android:fillColor="@color/actionMenuColor"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="@color/actionMenuColor"
android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:pathData="M6,17h3l2,-4L11,7L5,7v6h3zM14,17h3l2,-4L19,7h-6v6h3z"
android:fillColor="@color/actionMenuColor"/>
android:fillColor="@color/actionMenuColor"
android:pathData="M6,17h3l2,-4L11,7L5,7v6h3zM14,17h3l2,-4L19,7h-6v6h3z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z"
android:fillColor="@color/actionMenuColor"/>
android:fillColor="@color/actionMenuColor"
android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"
android:fillColor="@color/actionMenuColor"/>
android:fillColor="@color/actionMenuColor"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
</vector>
\ No newline at end of file
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="13dp"
android:height="16dp"
android:viewportWidth="13.0"
android:viewportHeight="16.0">
<path
android:pathData="M6.626,11.495L4.505,11.495L3.714,16L1.703,16L2.495,11.495L0,11.495L0,9.604L2.824,9.604L3.374,6.484L0.824,6.484L0.824,4.571L3.714,4.571L4.516,0L6.516,0L5.714,4.571L7.846,4.571L8.648,0L10.659,0L9.857,4.571L12.264,4.571L12.264,6.484L9.516,6.484L8.967,9.604L11.429,9.604L11.429,11.495L8.637,11.495L7.846,16L5.835,16L6.626,11.495ZM4.835,9.604L6.956,9.604L7.505,6.484L5.374,6.484L4.835,9.604Z"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#000000"
android:strokeWidth="1"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="197.218"
android:viewportHeight="197.218"
android:width="197.218dp"
android:height="197.218dp">
<group
android:translateX="-570.396"
android:translateY="-306.782">
<path
android:pathData="M704.445 306.782l-6.785 6.785c-6.084 6.084 -7.622 14.712 -4.309 21.871l-44.068 35.44 -3.086 -3.086c-7.889 -7.889 -19.525 -7.889 -27.414 0l-8.944 8.953 87.821 87.811 8.934 -8.933c7.899 -7.899 7.899 -19.525 0 -27.433l-3.076 -3.077 36.051 -44.68c6.824 2.466 14.367 1.036 20.037 -4.624l8.008 -5.858 -63.169 -63.169zm-66.867 116.487l-67.182 66.857 0 13.874 13.864 0 66.867 -67.182 -13.549 -13.549z"
android:fillColor="@color/actionMenuColor"
/>
</group>
</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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/AppTheme"
tools:context=".chatroom.ui.PinnedMessagesActivity">
<include
android:id="@+id/layout_app_bar"
layout="@layout/app_bar_chat_room" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/layout_app_bar" />
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout"
......@@ -12,23 +11,23 @@
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/message_list_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_typing_status"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer">
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<include
android:id="@+id/layout_message_list"
......@@ -44,52 +43,65 @@
android:layout_height="100dp"
android:src="@drawable/ic_chat_black_24dp"
android:tint="@color/icon_grey"
app:layout_constraintStart_toStartOf="parent"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/text_chat_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_chat_title"
app:layout_constraintVertical_chainStyle="packed"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/text_chat_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/msg_no_chat_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/image_chat_icon"
app:layout_constraintBottom_toTopOf="@id/text_chat_description"
android:textColor="@color/colorSecondaryText"
android:textSize="20sp"
android:layout_marginTop="24dp"
android:textStyle="bold"
android:textColor="@color/colorSecondaryText"
android:visibility="gone"
tools:visibility="visible"/>
app:layout_constraintBottom_toTopOf="@id/text_chat_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/image_chat_icon"
tools:visibility="visible" />
<TextView
android:id="@+id/text_chat_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/msg_no_chat_description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_chat_title"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:layout_marginTop="16dp"
android:text="@string/msg_no_chat_description"
android:textAlignment="center"
android:textSize="16sp"
android:textColor="@color/colorSecondaryTextLight"
android:textSize="16sp"
android:visibility="gone"
tools:visibility="visible"/>
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_chat_title"
tools:visibility="visible" />
<chat.rocket.android.widget.autocompletion.ui.SuggestionsView
android:id="@+id/suggestions_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/suggestion_background_color"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer" />
<TextView
android:id="@+id/text_typing_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:maxLines="2"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:background="@color/suggestion_background_color" />
app:layout_constraintEnd_toStartOf="parent" />
<include
android:id="@+id/layout_message_composer"
......@@ -102,18 +114,18 @@
android:id="@+id/view_dim"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:background="@color/colorDim"
android:visibility="gone" />
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer" />
<include
android:id="@+id/layout_message_attachment_options"
layout="@layout/message_attachment_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:layout_margin="5dp"
android:visibility="gone" />
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer" />
<TextView
android:id="@+id/connection_status_text"
......
......@@ -45,4 +45,15 @@
tools:text="connected"
tools:visibility="visible" />
<TextView
android:id="@+id/text_no_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="56dp"
android:text="@string/msg_no_search_found"
android:textSize="20sp"
android:layout_centerHorizontal="true"
android:visibility="gone"
tools:visibility="visible" />
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".favoritemessages.ui.FavoriteMessagesFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView
android:id="@+id/image_star"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_action_message_star_24dp"
android:tint="@color/icon_grey"
app:layout_constraintBottom_toTopOf="@+id/text_no_favorite_messages"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/text_no_favorite_messages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/no_favorite_messages"
android:textColor="@color/colorSecondaryText"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/text_no_favorite_messages_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image_star" />
<TextView
android:id="@+id/text_no_favorite_messages_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/no_favorite_description"
android:textAlignment="center"
android:textColor="@color/colorSecondaryTextLight"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_no_favorite_messages" />
<android.support.constraint.Group
android:id="@+id/no_messages_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="text_no_favorite_messages_description,image_star,text_no_favorite_messages"
tools:visibility="visible" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -3,18 +3,14 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:context=".pinnedmessages.ui.PinnedMessagesFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view_pinned"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
android:scrollbars="vertical" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
......@@ -36,48 +32,48 @@
android:id="@+id/iv_pin_icon"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_pin_black_24dp"
android:src="@drawable/ic_action_message_pin_24dp"
android:tint="@color/icon_grey"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_pin_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_pin_title"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/tv_pin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/no_pinned_messages"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_pin_icon"
app:layout_constraintBottom_toTopOf="@id/tv_pin_description"
android:textColor="@color/colorSecondaryText"
android:textSize="20sp"
android:layout_marginTop="24dp"
android:textStyle="bold"
android:textColor="@color/colorSecondaryText"/>
app:layout_constraintBottom_toTopOf="@id/tv_pin_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_pin_icon" />
<TextView
android:id="@+id/tv_pin_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_pinned_description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_pin_title"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="16dp"
android:text="@string/no_pinned_description"
android:textAlignment="center"
android:textColor="@color/colorSecondaryTextLight"
android:textSize="16sp"
android:textColor="@color/colorSecondaryTextLight"/>
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_pin_title" />
<android.support.constraint.Group
android:id="@+id/pin_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="tv_pin_description,iv_pin_icon,tv_pin_title"
android:visibility="gone"
app:constraint_referenced_ids="tv_pin_description,iv_pin_icon,tv_pin_title"
tools:visibility="visible" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -7,11 +7,11 @@
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding"
android:focusable="true"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:focusable="true">
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">
<include
android:id="@+id/layout_avatar"
......@@ -20,36 +20,39 @@
android:layout_height="40dp"
android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/new_messages_notif" />
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif" />
<LinearLayout
android:id="@+id/new_messages_notif"
tools:visibility="visible"
android:visibility="gone"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<View
android:layout_gravity="center"
android:layout_height="1dp"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="1dp"
android:layout_gravity="center"
android:layout_marginEnd="4dp"
android:background="@color/red"/>
android:layout_weight="1"
android:background="@color/red" />
<TextView
android:layout_width="wrap_content"
android:text="@string/msg_unread_messages"
android:layout_height="wrap_content"
android:text="@string/msg_unread_messages"
android:textColor="@color/red" />
<View
android:layout_gravity="center"
android:layout_height="1dp"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="1dp"
android:layout_gravity="center"
android:layout_marginStart="4dp"
android:background="@color/red"/>
android:layout_weight="1"
android:background="@color/red" />
</LinearLayout>
<LinearLayout
......@@ -58,8 +61,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@id/new_messages_notif"
app:layout_constraintLeft_toRightOf="@+id/layout_avatar">
app:layout_constraintLeft_toRightOf="@+id/layout_avatar"
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif">
<TextView
android:id="@+id/text_sender"
......@@ -75,6 +78,27 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
tools:text="11:45 PM" />
<TextView
android:id="@+id/text_edit_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/msg_edited"
android:textStyle="italic"
android:visibility="gone"
tools:visibility="visible" />
<ImageView
android:id="@+id/image_star_indicator"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:src="@drawable/ic_action_message_star_24dp"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
<TextView
......@@ -84,12 +108,13 @@
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="@id/top_container"
app:layout_constraintLeft_toLeftOf="@+id/top_container"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_container"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<include layout="@layout/layout_reactions"
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@+id/text_content"
......
<?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
......@@ -11,4 +11,9 @@
android:id="@+id/action_pinned_messages"
android:title="@string/title_pinned_messages"
app:showAsAction="never" />
<item
android:id="@+id/action_favorite_messages"
android:title="@string/title_favorite_messages"
app:showAsAction="never" />
</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:id="@+id/common_actions">
<item
android:id="@+id/action_menu_msg_reply"
android:icon="@drawable/ic_reply_black_24px"
android:id="@+id/action_message_reply"
android:icon="@drawable/ic_action_message_reply_24dp"
android:title="@string/action_msg_reply" />
<item
android:id="@+id/action_menu_msg_quote"
android:icon="@drawable/ic_quote_black_24px"
android:id="@+id/action_message_quote"
android:icon="@drawable/ic_action_message_quote_24dp"
android:title="@string/action_msg_quote" />
<item
android:id="@+id/action_menu_msg_edit"
android:icon="@drawable/ic_edit_black_24px"
android:title="@string/action_msg_edit" />
<item
android:id="@+id/action_menu_msg_copy"
android:icon="@drawable/ic_content_copy_black_24px"
android:id="@+id/action_message_copy"
android:icon="@drawable/ic_action_message_copy_24dp"
android:title="@string/action_msg_copy" />
<item
......@@ -33,20 +27,22 @@
<!--android:title="@string/action_msg_share" />-->
<item
android:id="@+id/action_menu_msg_pin_unpin"
android:icon="@drawable/ic_pin_black_24dp"
android:id="@+id/action_message_star"
android:icon="@drawable/ic_action_message_star_24dp"
android:title="@string/action_msg_star" />
<item
android:id="@+id/action_message_unpin"
android:icon="@drawable/ic_action_message_pin_24dp"
android:title="@string/action_msg_pin" />
<!--<item-->
<!--android:id="@+id/action_menu_msg_star"-->
<!--andrtextIconicon="@drawable/ic_star_black_24px"-->
<!--android:title="@string/action_msg_star" />-->
</group>
<item
android:id="@+id/action_message_edit"
android:icon="@drawable/ic_action_message_edit_24dp"
android:title="@string/action_msg_edit" />
<group android:id="@+id/dangerous_actions">
<item
android:id="@+id/action_menu_msg_delete"
android:icon="@drawable/ic_delete_black_24px"
android:id="@+id/action_message_delete"
android:icon="@drawable/ic_action_message_delete_24dp"
android:title="@string/action_msg_delete" />
</group>
</menu>
\ No newline at end of file
......@@ -100,7 +100,6 @@
<string name="msg_ver_not_minimum">
Parece que la versión del servidor está por debajo de la versión mínima requerida %1$s.\nActualice su servidor para iniciar sesión!
</string>
<string name="msg_proceed">PROCEDER</string>
<string name="msg_cancel">CANCELAR</string>
<string name="msg_warning">ADVERTENCIA</string>
......@@ -111,6 +110,16 @@
<string name="msg_image_saved_failed">Error al guardar la imagen</string>
<string name="msg_no_chat_title">Sin mensajes de chat</string>
<string name="msg_no_chat_description">Comience a conversar para ver\nsus mensajes aquí.</string>
<string name="msg_edited">(editado)</string>
// TODO: Add proper translation.
<string name="msg_and">\u0020and\u0020</string>
// TODO: Add proper translation.
<string name="msg_is_typing">\u0020is typing…</string>
// TODO: Add proper translation.
<string name="msg_are_typing">\u0020are typing…</string>
// TODO: Add proper translation.
<string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">No se han encontrado resultados</string>
<!-- System messages -->
<string name="message_room_name_changed">Nombre de la sala cambiado para: %1$s por %2$s</string>
......@@ -121,6 +130,10 @@
<string name="message_welcome">Bienvenido %s</string>
<string name="message_removed">Mensaje eliminado</string>
<string name="message_pinned">Fijado una mensaje:</string>
<string name="message_muted">Usuario %1$s silenciado por %2$s</string>
<string name="message_unmuted">Usuario %1$s no silenciado por %2$s</string>
<string name="message_role_add">%1$s fue establecido %2$s por %3$s</string>
<string name="message_role_removed">%1$s ya no es %2$s por %3$s</string>
<!-- Message actions -->
<string name="action_msg_reply">Respuesta</string>
......@@ -131,6 +144,8 @@
<string name="action_msg_pin">Fijar mensaje</string>
<string name="action_msg_unpin">Soltar mensaje</string>
<string name="action_msg_star">Star mensaje</string>
// TODO: Add proper translation.
<string name="action_msg_unstar">Unstar Message</string>
<string name="action_msg_share">Compartir</string>
<string name="action_title_editing">Edición de mensaje</string>
<string name="action_msg_add_reaction">Añadir una reacción</string>
......@@ -139,6 +154,8 @@
<string name="permission_editing_not_allowed">La edición no és permitida</string>
<string name="permission_deleting_not_allowed">Eliminar no és permitido</string>
<string name="permission_pinning_not_allowed">Fijar no és permitido</string>
// TODO: Add proper translation.
<string name="permission_starring_not_allowed">Starring is not allowed</string>
<!-- Members List -->
<string name="title_members_list">Lista de miembros</string>
......@@ -146,7 +163,13 @@
<!-- Pinned Messages -->
<string name="title_pinned_messages">Mensajes fijados</string>
<string name="no_pinned_messages">Sin mensajes fijadas</string>
<string name="no_pinned_description">Todas las mensajes fijadas\naparecen aquí.</string>
<string name="no_pinned_description">Todas las mensajes fijadas\naparecen aquí</string>
<!-- Favorite Messages -->
<!-- TODO Add proper translation-->
<string name="title_favorite_messages">Favorite Messages</string>
<string name="no_favorite_messages">No favorite messages</string>
<string name="no_favorite_description">All the favorite messages\nappear here</string>
<!-- Upload Messages -->
<string name="max_file_size_exceeded">Tamaño del archivo (%1$d bytes) excedió el tamaño máximo de carga de %2$d bytes</string>
......
......@@ -110,6 +110,16 @@
<string name="msg_image_saved_failed">Échec de l\'enregistrement de l\'image</string>
<string name="msg_no_chat_title">Aucun message de discussion</string>
<string name="msg_no_chat_description">Commencez à converser pour voir\nvos messages ici.</string>
<string name="msg_edited">(édité)</string>
// TODO: Add proper translation.
<string name="msg_and">\u0020and\u0020</string>
// TODO: Add proper translation.
<string name="msg_is_typing">\u0020is typing…</string>
// TODO: Add proper translation.
<string name="msg_are_typing">\u0020are typing…</string>
// TODO: Add proper translation.
<string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">Aucun résultat trouvé</string>
<!-- System messages -->
<string name="message_room_name_changed">Le nom de le salle a changé à: %1$s par %2$s</string>
......@@ -120,6 +130,10 @@
<string name="message_welcome">Bienvenue %s</string>
<string name="message_removed">Message supprimé</string>
<string name="message_pinned">Épinglé un message:</string>
<string name="message_muted">Utilisateur %1$s mis en sourdine par %2$s</string>
<string name="message_unmuted">Utilisateur %1$s non muté par %2$s</string>
<string name="message_role_add">%1$s a été défini %2$s par %3$s</string>
<string name="message_role_removed">%1$s is no longer %2$s par %3$s</string>
<!-- Message actions -->
<string name="action_msg_reply">Répondre</string>
......@@ -129,7 +143,10 @@
<string name="action_msg_delete">Effacer</string>
<string name="action_msg_pin">Épingle message</string>
<string name="action_msg_unpin">Enlever message</string>
// TODO: Add proper translation.
<string name="action_msg_star">Star message</string>
// TODO: Add proper translation.
<string name="action_msg_unstar">Unstar Message</string>
<string name="action_msg_share">Partager</string>
<string name="action_title_editing">Modification du message</string>
<string name="action_msg_add_reaction">Ajouter une réaction</string>
......@@ -138,6 +155,8 @@
<string name="permission_editing_not_allowed">L\'édition n\'est pas autorisée</string>
<string name="permission_deleting_not_allowed">La suppression n\'est pas autorisée</string>
<string name="permission_pinning_not_allowed">L\'épinglage n\'est pas autorisé</string>
// TODO: Add proper translation.
<string name="permission_starring_not_allowed">Starring is not allowed</string>
<!-- Members List -->
<string name="title_members_list">Liste des membres</string>
......@@ -145,7 +164,13 @@
<!-- Pinned Messages -->
<string name="title_pinned_messages">Messages épinglés</string>
<string name="no_pinned_messages">Aucun message épinglé</string>
<string name="no_pinned_description">Tous les messages épinglés\napparaissent ici.</string>
<string name="no_pinned_description">Tous les messages épinglés\napparaissent ici</string>
<!-- Favorite Messages -->
<!-- TODO Add proper translation-->
<string name="title_favorite_messages">Favorite Messages</string>
<string name="no_favorite_messages">No favorite messages</string>
<string name="no_favorite_description">All the favorite messages\nappear here</string>
<!-- Upload Messages -->
<string name="max_file_size_exceeded">Taille du fichier (%1$d bytes) dépassé la taille de téléchargement maximale de %2$d bytes</string>
......
......@@ -112,6 +112,16 @@
<string name="msg_image_saved_failed">छवि को सहेजने में विफल</string>
<string name="msg_no_chat_title">कोई चैट संदेश नहीं</string>
<string name="msg_no_chat_description">यहां अपने संदेश देखने के लिए\nबातचीत शुरू करें।</string>
<string name="msg_edited">(संपादित)</string>
// TODO: Add proper translation.
<string name="msg_and">\u0020and\u0020</string>
// TODO: Add proper translation.
<string name="msg_is_typing">\u0020is typing…</string>
// TODO: Add proper translation.
<string name="msg_are_typing">\u0020are typing…</string>
// TODO: Add proper translation.
<string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">कोई परिणाम नहीं मिला</string>
<!-- System messages -->
<string name="message_room_name_changed">%2$s ने रूम का नाम बदलकर %1$s किया</string>
......@@ -122,6 +132,10 @@
<string name="message_welcome">%s का स्वागत करते हैं</string>
<string name="message_removed">संदेश हटाया गया</string>
<string name="message_pinned">एक संदेश पिन किया:</string>
<string name="message_muted">उपयोगकर्ता %1$s %2$s द्वारा म्यूट किया गया</string>
<string name="message_unmuted">उपयोगकर्ता %1$s %2$s द्वारा अनम्यूट किया गया</string>
<string name="message_role_add">%1$s %3$s द्वारा %2$s सेट किया गया था</string>
<string name="message_role_removed">%1$s अब %3$s द्वारा %2$s नहीं है</string>
<!-- Message actions -->
<string name="action_msg_reply">जवाब दें</string>
......@@ -132,6 +146,8 @@
<string name="action_msg_pin">संदेश को पिन करें</string>
<string name="action_msg_unpin">संदेश को पिन से हटाएँ</string>
<string name="action_msg_star">संदेश को स्टार करें</string>
// TODO: Add proper translation.
<string name="action_msg_unstar">Unstar Message</string>
<string name="action_msg_share">शेयर करें</string>
<string name="action_title_editing">संपादन संदेश</string>
<string name="action_msg_add_reaction">प्रतिक्रिया जोड़ें</string>
......@@ -140,6 +156,8 @@
<string name="permission_editing_not_allowed">संपादन की अनुमति नहीं है</string>
<string name="permission_deleting_not_allowed">हटाने की अनुमति नहीं है</string>
<string name="permission_pinning_not_allowed">पिनि करने की अनुमति नहीं है</string>
// TODO: Add proper translation.
<string name="permission_starring_not_allowed">Starring is not allowed</string>
<!-- Members List -->
<string name="title_members_list">सदस्यों की सूची</string>
......@@ -149,6 +167,12 @@
<string name="no_pinned_messages">कोई पिन संदेश नहीं</string>
<string name="no_pinned_description">सभी पिन किए गए संदेश यहां\nदिखाई देते हैं।</string>
<!-- Favorite Messages -->
<!-- TODO Add proper translation-->
<string name="title_favorite_messages">Favorite Messages</string>
<string name="no_favorite_messages">No favorite messages</string>
<string name="no_favorite_description">All the favorite messages\nappear here</string>
<!-- Upload Messages -->
<string name="max_file_size_exceeded">फ़ाइल का आकार %1$d बाइट्स ने %2$d बाइट्स के अधिकतम अपलोड आकार को पार कर लिया है</string>
......
......@@ -106,16 +106,26 @@
<string name="msg_invalid_server_protocol">O protocolo selecionado não é suportado pelo servidor, por favor utilize HTTPS e tente novamente</string>
<string name="msg_image_saved_successfully">Imagem salva na galeria</string>
<string name="msg_image_saved_failed">Falha ao salvar a imagem</string>
<string name="msg_edited">(editado)</string>
<string name="msg_and">\u0020e\u0020</string>
<string name="msg_is_typing">\u0020está digitando…</string>
<string name="msg_are_typing">\u0020estão digitando…</string>
<string name="msg_several_users_are_typing">Vários usuários estão digitando…</string>
<string name="msg_no_search_found">nenhum resultado encontrado</string>
<!-- System messages -->
<string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string>
<string name="message_user_added_by">Usuário %1$s adicionado por %2$s</string>
<string name="message_user_removed_by">Usuário %1$s removido por %2$s</string>
<string name="message_user_left">Saiu da sala.</string>
<string name="message_user_joined_channel">Entrou na sala.</string>
<string name="message_user_left">Saiu da sala</string>
<string name="message_user_joined_channel">Entrou na sala</string>
<string name="message_welcome">Bem-vindo, %s</string>
<string name="message_removed">Mensagem removida</string>
<string name="message_pinned">Pinou uma mensagem:</string>
<string name="message_muted">Usuário %1$s entrou no modo mudo por %2$s</string>
<string name="message_unmuted">Usuário %1$s saiu do modo mudo por %2$s</string>
<string name="message_role_add">%1$s foi definido %2$s por %3$s</string>
<string name="message_role_removed">%1$s não é mais %2$s por %3$s</string>
<!-- Message actions -->
<string name="action_msg_reply">Responder</string>
......@@ -123,17 +133,19 @@
<string name="action_msg_copy">Copiar</string>
<string name="action_msg_quote">Citar</string>
<string name="action_msg_delete">Remover</string>
<string name="action_msg_pin">Fixar Mensagem</string>
<string name="action_msg_unpin">Desafixar Mensagem</string>
<string name="action_msg_star">Favoritar Mensagem</string>
<string name="action_msg_pin">Pinar mensagem</string>
<string name="action_msg_unpin">Despinar mensagem</string>
<string name="action_msg_star">Favoritar mensagem</string>
<string name="action_msg_unstar">Desfavoritar messagem</string>
<string name="action_msg_share">Compartilhar</string>
<string name="action_title_editing">Editando Mensagem</string>
<string name="action_title_editing">Editando mensagem</string>
<string name="action_msg_add_reaction">Adicionar reação</string>
<!-- Permission messages -->
<string name="permission_editing_not_allowed">Edição não permitida</string>
<string name="permission_deleting_not_allowed">Remoção não permitida</string>
<string name="permission_pinning_not_allowed">Pinagem não permitida</string>
<string name="permission_starring_not_allowed">Favoritar não permitido</string>
<!-- Members List -->
<string name="title_members_list">Lista de Membros</string>
......@@ -143,12 +155,18 @@
<string name="no_pinned_messages">Nenhuma mensagem pinada</string>
<string name="no_pinned_description">Todas as mensagens pinadas\naparecerão aqui</string>
<!-- Favorite Messages -->
<!-- TODO Add proper translation-->
<string name="title_favorite_messages">Messagens Favoritas</string>
<string name="no_favorite_messages">Nenhuma messagem favorita</string>
<string name="no_favorite_description">Todas as mensagens favoritas\naparecerão aqui</string>
<!-- Upload Messages -->
<string name="max_file_size_exceeded">Tamanho de arquivo (%1$d bytes) excedeu tamanho máximo de upload (%2$d bytes)</string>
<!-- Socket status -->
<string name="status_connected">Conectado</string>
<string name="status_disconnected">Desconetado</string>
<string name="status_disconnected">Desconectado</string>
<string name="status_connecting">Conectando</string>
<string name="status_authenticating">Autenticando</string>
<string name="status_disconnecting">Desconectando</string>
......@@ -165,7 +183,7 @@
<string name="Slash_Tableflip_Description">Exibir (╯°□°)╯︵ ┻━┻</string>
<string name="Slash_TableUnflip_Description">Exibir ┬─┬ ノ( ゜-゜ノ)</string>
<string name="Create_A_New_Channel">Criar um novo canal</string>
<string name="Show_the_keyboard_shortcut_list">Show the keyboard shortcut list</string>
<string name="Show_the_keyboard_shortcut_list">Exibir a lista de atalhos do teclado</string>
<string name="Invite_user_to_join_channel_all_from"> do [#canal] para entrar neste</string>
<string name="Invite_user_to_join_channel_all_to">Convidar todos os usuários deste canal para entrar no [#canal]</string>
<string name="Archive">Arquivar</string>
......@@ -173,8 +191,8 @@
<string name="Leave_the_current_channel">Sair do canal atual</string>
<string name="Displays_action_text">Exibir texto de ação</string>
<string name="Direct_message_someone">Enviar DM para alguém</string>
<string name="Mute_someone_in_room">Silenciar alguém</string>
<string name="Unmute_someone_in_room">De-silenciar alguém na sala</string>
<string name="Mute_someone_in_room">Ativar o modo mudo em alguém</string>
<string name="Unmute_someone_in_room">Desativar o modo mudo em alguém na sala</string>
<string name="Invite_user_to_join_channel">Convidar algum usuário para entrar neste canal</string>
<string name="Unarchive">Desarquivar</string>
<string name="Join_the_given_channel">Entrar no canal especificado</string>
......
......@@ -107,6 +107,12 @@
<string name="msg_invalid_server_protocol">The selected protocol is not accepted by this server, try using HTTPS</string>
<string name="msg_image_saved_successfully">Image has been saved to gallery</string>
<string name="msg_image_saved_failed">Failed to save image</string>
<string name="msg_edited">(edited)</string>
<string name="msg_and">\u0020and\u0020</string>
<string name="msg_is_typing">\u0020is typing…</string>
<string name="msg_are_typing">\u0020are typing…</string>
<string name="msg_several_users_are_typing">Several users are typing…</string>
<string name="msg_no_search_found">No result found</string>
<!-- System messages -->
<string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string>
......@@ -117,6 +123,10 @@
<string name="message_welcome">Welcome %s</string>
<string name="message_removed">Message removed</string>
<string name="message_pinned">Pinned a message:</string>
<string name="message_muted">User %1$s muted by %2$s</string>
<string name="message_unmuted">User %1$s unmuted by %2$s</string>
<string name="message_role_add">%1$s was set %2$s by %3$s</string>
<string name="message_role_removed">%1$s is no longer %2$s by %3$s</string>
<!-- Message actions -->
<string name="action_msg_reply">Reply</string>
......@@ -127,6 +137,7 @@
<string name="action_msg_pin">Pin Message</string>
<string name="action_msg_unpin">Unpin Message</string>
<string name="action_msg_star">Star Message</string>
<string name="action_msg_unstar">Unstar Message</string>
<string name="action_msg_share">Share</string>
<string name="action_title_editing">Editing Message</string>
<string name="action_msg_add_reaction">Add reaction</string>
......@@ -135,6 +146,7 @@
<string name="permission_editing_not_allowed">Editing is not allowed</string>
<string name="permission_deleting_not_allowed">Deleting is not allowed</string>
<string name="permission_pinning_not_allowed">Pinning is not allowed</string>
<string name="permission_starring_not_allowed">Starring is not allowed</string>
<!-- Members List -->
<string name="title_members_list">Members List</string>
......@@ -142,7 +154,12 @@
<!-- Pinned Messages -->
<string name="title_pinned_messages">Pinned Messages</string>
<string name="no_pinned_messages">No pinned messages</string>
<string name="no_pinned_description">All the pinned messages\nappear here.</string>
<string name="no_pinned_description">All the pinned messages\nappear here</string>
<!-- Favorite Messages -->
<string name="title_favorite_messages">Favorite Messages</string>
<string name="no_favorite_messages">No favorite messages</string>
<string name="no_favorite_description">All the favorite messages\nappear here</string>
<!-- Upload Messages -->
<string name="max_file_size_exceeded">File size %1$d bytes exceeded max upload size of %2$d bytes</string>
......
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
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