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

Merge branch 'develop' into wear-module

parents 2fa3abb1 ca99f416
......@@ -13,7 +13,7 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 2020
versionCode 2021
versionName "2.2.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
......
......@@ -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
......@@ -15,6 +29,7 @@ import java.security.InvalidParameterException
class ChatRoomAdapter(
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null
......@@ -64,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()}")
}
......@@ -115,6 +136,8 @@ class ChatRoomAdapter(
holder.bind(dataSet[position] as ColorAttachmentViewModel)
is GenericFileAttachmentViewHolder ->
holder.bind(dataSet[position] as GenericFileAttachmentViewModel)
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyViewModel)
}
}
......@@ -196,10 +219,10 @@ class ChatRoomAdapter(
message.apply {
when (item.itemId) {
R.id.action_message_reply -> {
presenter?.citeMessage(roomType, id, true)
presenter?.citeMessage(roomName, roomType, id, true)
}
R.id.action_message_quote -> {
presenter?.citeMessage(roomType, id, false)
presenter?.citeMessage(roomName, roomType, id, false)
}
R.id.action_message_copy -> {
presenter?.copyMessage(id)
......
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageReplyViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message_reply.view.*
class MessageReplyViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
private val replyCallback: (roomName: String, permalink: String) -> Unit
) : BaseViewHolder<MessageReplyViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(itemView)
}
}
override fun bindViews(data: MessageReplyViewModel) {
with(itemView) {
button_message_reply.setOnClickListener {
with(data.rawData) {
replyCallback.invoke(roomName, permalink)
}
}
}
}
}
\ No newline at end of file
......@@ -37,7 +37,7 @@ class MessageViewHolder(
if (data.isTemporary) Color.GRAY else Color.BLACK
)
data.message.let {
text_edit_indicator.isVisible = it.isSystemMessage() && it.editedBy != null
text_edit_indicator.isVisible = !it.isSystemMessage() && it.editedBy != null
image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false
}
}
......
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
......@@ -30,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
......@@ -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 {
......@@ -122,9 +123,9 @@ interface ChatRoomView : LoadingView, MessageView {
/**
* This user has joined the chat callback.
*
* @param canPost Whether the user can post a message or not.
* @param userCanPost Whether the user can post a message or not.
*/
fun onJoined(canPost: Boolean)
fun onJoined(userCanPost: Boolean)
fun showReactionsPopup(messageId: String)
......@@ -135,5 +136,14 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>)
fun onRoomChanged(canPost: Boolean)
/**
* Communicate whether it's a broadcast channel and if current user can post to it.
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
/**
* Open a DM with the user in the given [chatRoom] and pass the [permalink] for the message
* to reply.
*/
fun openDirectMessage(chatRoom: ChatRoom, permalink: String)
}
\ No newline at end of file
......@@ -60,8 +60,16 @@ class MessageService : JobService() {
)
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
......
......@@ -10,11 +10,14 @@ import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject
fun Context.chatRoomIntent(
......@@ -24,7 +27,8 @@ fun Context.chatRoomIntent(
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean = true,
isChatRoomOwner: Boolean = false
isChatRoomCreator: Boolean = false,
chatRoomMessage: String? = null
): Intent {
return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
......@@ -33,7 +37,8 @@ fun Context.chatRoomIntent(
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_OWNER, isChatRoomOwner)
putExtra(INTENT_CHAT_ROOM_IS_CREATOR, isChatRoomCreator)
putExtra(INTENT_CHAT_ROOM_MESSAGE, chatRoomMessage)
}
}
......@@ -41,9 +46,10 @@ private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
private const val INTENT_CHAT_ROOM_IS_READ_ONLY = "chat_room_is_read_only"
private const val INTENT_CHAT_ROOM_IS_OWNER = "chat_room_is_owner"
private const val INTENT_CHAT_ROOM_IS_CREATOR = "chat_room_is_creator"
private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val INTENT_CHAT_IS_SUBSCRIBED = "is_chat_room_subscribed"
private const val INTENT_CHAT_ROOM_MESSAGE = "chat_room_message"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject
......@@ -62,7 +68,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private var isChatRoomSubscribed: Boolean = true
private var isChatRoomOwner: Boolean = false
private var isChatRoomCreator: Boolean = false
private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) {
......@@ -91,8 +97,10 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true)
requireNotNull(isChatRoomReadOnly) { "no chat_room_is_read_only provided in Intent extras" }
isChatRoomOwner = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_OWNER, false)
requireNotNull(isChatRoomOwner) { "no chat_room_is_owner 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()
......@@ -102,10 +110,8 @@ 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, isChatRoomOwner
)
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage)
}
}
}
......@@ -120,13 +126,45 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
toolbar.title = chatRoomName
text_room_name.textContent = chatRoomName
showRoomTypeIcon(true)
toolbar.setNavigationOnClickListener { finishActivity() }
}
fun showRoomTypeIcon(showRoomTypeIcon: Boolean) {
if (showRoomTypeIcon) {
val roomType = roomTypeOf(chatRoomType)
val drawable = when (roomType) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, this)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, this)
}
is RoomType.DirectMessage -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, this)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.white)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
}
} else {
text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
}
fun setupToolbarTitle(toolbarTitle: String) {
supportActionBar?.title = toolbarTitle
text_room_name.textContent = toolbarTitle
}
private fun finishActivity() {
......
......@@ -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
......@@ -313,7 +365,6 @@ class ViewModelMapper @Inject constructor(
}
private fun getSystemMessage(message: Message): CharSequence {
println(message)
val content = when (message.type) {
//TODO: Add implementation for Welcome type.
is MessageType.MessageRemoved -> context.getString(R.string.message_removed)
......
......@@ -9,7 +9,18 @@ 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
......@@ -31,9 +42,13 @@ 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
......@@ -43,7 +58,7 @@ 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,
......@@ -148,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)
......@@ -253,7 +268,7 @@ class ChatRoomsPresenter @Inject constructor(
fun updateSortedChatRooms() {
launchUI(strategy) {
val roomList = getChatRoomsInteractor.getAll(currentServer)
val roomList = chatRoomsInteractor.getAll(currentServer)
view.updateChatRooms(sortRooms(roomList))
}
}
......@@ -453,7 +468,7 @@ 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(
......@@ -493,7 +508,7 @@ 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(
......@@ -532,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) {
......@@ -573,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,
......@@ -600,10 +615,10 @@ class ChatRoomsPresenter @Inject constructor(
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)))
}
}
}
......@@ -613,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)
......
......@@ -45,7 +45,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var localRepository: LocalRepository
private lateinit var preferences: SharedPreferences
private var searchView: SearchView? = null
private val handler = Handler()
......@@ -60,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() {
......
......@@ -2,7 +2,7 @@ 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.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
......@@ -16,7 +16,7 @@ class FavoriteMessagesPresenter @Inject constructor(
private val view: FavoriteMessagesView,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor,
private val roomsInteractor: ChatRoomsInteractor,
private val mapper: ViewModelMapper,
factory: RocketChatClientFactory
) {
......
......@@ -70,7 +70,7 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
override fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>) {
ui {
if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, null, false)
adapter = ChatRoomAdapter(chatRoomType, "", null, false)
recycler_view.adapter = adapter
val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
......
package chat.rocket.android.helper
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.useRealName
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Message
import javax.inject.Inject
class MessageHelper @Inject constructor(
getSettingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor
) {
private val currentServer = serverInteractor.get()!!
private val settings: PublicSettings = getSettingsInteractor.get(currentServer)
fun createPermalink(message: Message, chatRoom: ChatRoom): String {
val type = when (chatRoom.type) {
is RoomType.PrivateGroup -> "group"
is RoomType.Channel -> "channel"
is RoomType.DirectMessage -> "direct"
is RoomType.Livechat -> "livechat"
else -> "custom"
}
val name = if (settings.useRealName()) chatRoom.fullName ?: chatRoom.name else chatRoom.name
return "[ ]($currentServer/$type/$name?msg=${message.id}) "
}
fun messageIdFromPermalink(permalink: String): String? {
PERMALINK_REGEX.find(permalink.trim())?.let {
if (it.groupValues.size == 5) {
return it.groupValues[MESSAGE_ID]
}
}
return null
}
fun roomNameFromPermalink(permalink: String): String? {
PERMALINK_REGEX.find(permalink.trim())?.let {
if (it.groupValues.size == 5) {
return it.groupValues[ROOM_NAME]
}
}
return null
}
fun roomTypeFromPermalink(permalink: String): String? {
PERMALINK_REGEX.find(permalink.trim())?.let {
if (it.groupValues.size == 5) {
val type = it.groupValues[ROOM_TYPE]
return when(type) {
"group" -> "p"
"channel" -> "c"
"direct" -> "d"
"livechat" -> "l"
else -> type
}
}
}
return null
}
companion object {
private const val ROOM_TYPE = 2
private const val ROOM_NAME = 3
private const val MESSAGE_ID = 4
val PERMALINK_REGEX = "(?:__|[*#])|\\[(.+?)\\]\\(.+?//.+?/(.+)/(.+)\\?.*=(.*)\\)".toRegex()
}
}
\ No newline at end of file
......@@ -48,7 +48,7 @@ class UserHelper @Inject constructor(
/**
* Return the username for the current logged [User].
*/
fun username(): String? = user()?.username
fun username(): String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY, null)
/**
* Whether current [User] is admin on the current server.
......
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
......@@ -37,9 +36,9 @@ class MainNavigator(internal val activity: MainActivity) {
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean,
isChatRoomOwner: Boolean) {
isChatRoomCreator: Boolean) {
activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomOwner))
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomCreator))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
......
......@@ -2,7 +2,7 @@ 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
......@@ -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
) {
......
......@@ -73,7 +73,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
ui {
if (recycler_view_pinned.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, null, false)
adapter = ChatRoomAdapter(chatRoomType, "", null, false)
recycler_view_pinned.adapter = adapter
val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
......
......@@ -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 }
}
/**
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M22.548,9l0.452,-2h-5.364l1.364,-6h-2l-1.364,6h-5l1.364,-6h-2l-1.364,6h-6.184l-0.452,2h6.182l-1.364,6h-5.36l-0.458,2h5.364l-1.364,6h2l1.364,-6h5l-1.364,6h2l1.364,-6h6.185l0.451,-2h-6.182l1.364,-6h5.366zM13.818,15h-5l1.364,-6h5l-1.364,6z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="20.0"
android:viewportWidth="20.0">
<path
android:fillColor="#FF000000"
android:pathData="M13.6,13.47c-0.91,0.953 -2.191,1.545 -3.61,1.545 -2.756,0 -4.99,-2.234 -4.99,-4.99 0,-0.009 0,-0.018 0,-0.026v0.001c0,-2.761 2.239,-5 5,-5 1.131,0 2.175,0.376 3.013,1.009l-0.013,-0.009v-1h2v6.5c0,0.828 0.672,1.5 1.5,1.5s1.5,-0.672 1.5,-1.5v0,-1.5c-0.003,-4.416 -3.584,-7.994 -8,-7.994 -4.418,0 -8,3.582 -8,8s3.582,8 8,8c1.305,0 2.537,-0.312 3.625,-0.867l-0.045,0.021 0.9,1.79c-1.305,0.668 -2.847,1.06 -4.48,1.06 -5.523,0 -10,-4.477 -10,-10s4.477,-10 10,-10c5.519,0 9.994,4.472 10,9.99v0.001h-0.01v1.5c0,0.003 0,0.007 0,0.01 0,1.933 -1.567,3.5 -3.5,3.5 -1.202,0 -2.262,-0.606 -2.892,-1.528l-0.008,-0.012zM10,13c1.657,0 3,-1.343 3,-3s-1.343,-3 -3,-3v0c-1.657,0 -3,1.343 -3,3s1.343,3 3,3v0z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M18,10v-4c0,-3.313 -2.687,-6 -6,-6s-6,2.687 -6,6v4h-3v14h18v-14h-3zM8,6c0,-2.206 1.794,-4 4,-4s4,1.794 4,4v4h-8v-4zM19,22h-14v-10h14v10z" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="4dp" />
<stroke android:color="#1D74F5" android:width="2dp" />
</shape>
\ No newline at end of file
......@@ -8,7 +8,7 @@
<include
android:id="@+id/layout_app_bar"
layout="@layout/app_bar" />
layout="@layout/app_bar_chat_room" />
<FrameLayout
android:id="@+id/fragment_container"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationIcon="?android:attr/homeAsUpIndicator"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.constraint.ConstraintLayout
android:id="@+id/toolbar_content_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- TODO implement -->
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_room_avatar"
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:roundAsCircle="true" />
<TextView
android:id="@+id/text_room_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:drawablePadding="@dimen/text_view_drawable_padding"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Developers" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/message_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding">
<Button
android:id="@+id/button_message_reply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginTop="5dp"
android:layout_marginStart="56dp"
android:background="@drawable/message_reply_button_bg"
android:text="@string/action_msg_reply"
android:textAllCaps="false"
android:textColor="#1D74F5"
android:textSize="14sp"
app:layout_constraintLeft_toLeftOf="parent" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@+id/button_message_reply"
app:layout_constraintStart_toStartOf="@+id/button_message_reply"
app:layout_constraintTop_toBottomOf="@+id/button_message_reply" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
package chat.rocket.android
import chat.rocket.android.server.infraestructure.MemoryMessagesRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
import kotlinx.coroutines.experimental.runBlocking
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Test
import org.hamcrest.CoreMatchers.`is` as isEqualTo
class MemoryMessagesRepositoryTest {
val repository = MemoryMessagesRepository()
val msg = Message(
id = "messageId",
roomId = "GENERAL",
message = "Beam me up, Scotty.",
timestamp = 1511443964815,
attachments = null,
sender = null,
avatar = null,
channels = null,
editedAt = null,
editedBy = null,
groupable = true,
mentions = null,
parseUrls = false,
senderAlias = null,
type = MessageType.MessageRemoved(),
updatedAt = 1511443964815,
urls = null,
pinned = false,
reactions = null
)
val msg2 = Message(
id = "messageId2",
roomId = "sandbox",
message = "Highly Illogical",
timestamp = 1511443964818,
attachments = null,
sender = null,
avatar = null,
channels = null,
editedAt = null,
editedBy = null,
groupable = true,
mentions = null,
parseUrls = false,
senderAlias = null,
type = MessageType.MessageRemoved(),
updatedAt = 1511443964818,
urls = null,
pinned = false,
reactions = null
)
@Before
fun setup() {
runBlocking {
repository.clear()
}
}
@Test
fun `save() should save a single message`() {
runBlocking {
assertThat(repository.getAll().size, isEqualTo(0))
repository.save(msg)
val allMessages = repository.getAll()
assertThat(allMessages.size, isEqualTo(1))
allMessages[0].apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
}
}
@Test
fun `saveAll() should all saved messages`() {
runBlocking {
assertThat(repository.getAll().size, isEqualTo(0))
repository.saveAll(listOf(msg, msg2))
val allMessages = repository.getAll()
assertThat(allMessages.size, isEqualTo(2))
allMessages[0].apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
allMessages[1].apply {
assertThat(id, isEqualTo("messageId2"))
assertThat(message, isEqualTo("Highly Illogical"))
assertThat(roomId, isEqualTo("sandbox"))
}
}
}
@Test
fun `getById() should return a single message`() {
runBlocking {
repository.saveAll(listOf(msg, msg2))
var singleMsg = repository.getById("messageId")
assertThat(singleMsg, notNullValue())
singleMsg!!.apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
singleMsg = repository.getById("messageId2")
assertThat(singleMsg, notNullValue())
singleMsg!!.apply {
assertThat(id, isEqualTo("messageId2"))
assertThat(message, isEqualTo("Highly Illogical"))
assertThat(roomId, isEqualTo("sandbox"))
}
}
}
@Test
fun `getByRoomId() should return all messages for room id or an empty list`() {
runBlocking {
repository.saveAll(listOf(msg, msg2))
var roomMessages = repository.getByRoomId("faAad32fkasods2")
assertThat(roomMessages.isEmpty(), isEqualTo(true))
roomMessages = repository.getByRoomId("sandbox")
assertThat(roomMessages.size, isEqualTo(1))
roomMessages[0].apply {
assertThat(id, isEqualTo("messageId2"))
assertThat(message, isEqualTo("Highly Illogical"))
assertThat(roomId, isEqualTo("sandbox"))
}
roomMessages = repository.getByRoomId("GENERAL")
assertThat(roomMessages.size, isEqualTo(1))
roomMessages[0].apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
}
}
}
\ No newline at end of file
......@@ -4,3 +4,4 @@ distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
distributionSha256Sum=9af7345c199f1731c187c96d3fe3d31f5405192a42046bafa71d846c3d9adacb
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment