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

Merge pull request #1338 from RocketChat/new/read-receipts

[NEW] Add read receipt support
parents d2468b7b a47b84f0
......@@ -74,6 +74,11 @@
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".chatinformation.ui.MessageInfoActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<!-- TODO: Change to fragment-->
<activity
android:name=".settings.password.ui.PasswordActivity"
......
package chat.rocket.android.chatinformation.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatinformation.adapter.ReadReceiptAdapter.ReadReceiptViewHolder
import chat.rocket.android.chatinformation.viewmodel.ReadReceiptViewModel
import chat.rocket.android.util.extensions.inflate
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_read_receipt.view.*
class ReadReceiptAdapter : RecyclerView.Adapter<ReadReceiptViewHolder>() {
private val data = ArrayList<ReadReceiptViewModel>()
init {
setHasStableIds(true)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReadReceiptViewHolder {
return ReadReceiptViewHolder(parent.inflate(R.layout.item_read_receipt, false))
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: ReadReceiptViewHolder, position: Int) {
holder.bind(data[position])
}
fun addAll(items: List<ReadReceiptViewModel>) {
data.clear()
data.addAll(items)
notifyItemRangeInserted(0, items.size)
}
class ReadReceiptViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(readReceipt: ReadReceiptViewModel) {
with(itemView) {
image_avatar.setImageURI(readReceipt.avatar)
receipt_name.text = readReceipt.name
receipt_time.text = readReceipt.time
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatinformation.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatinformation.presentation.MessageInfoView
import chat.rocket.android.chatinformation.ui.MessageInfoFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class MessageInfoFragmentModule {
@Provides
fun messageInfoView(frag: MessageInfoFragment): MessageInfoView {
return frag
}
@Provides
fun provideLifecycleOwner(frag: MessageInfoFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
\ No newline at end of file
package chat.rocket.android.chatinformation.di
import chat.rocket.android.chatinformation.ui.MessageInfoFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class MessageInfoFragmentProvider {
@ContributesAndroidInjector(modules = [MessageInfoFragmentModule::class])
abstract fun provideMessageInfoFragment(): MessageInfoFragment
}
\ No newline at end of file
package chat.rocket.android.chatinformation.presentation
import chat.rocket.android.chatinformation.viewmodel.ReadReceiptViewModel
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.getMessageReadReceipts
import chat.rocket.core.internal.rest.queryUsers
import timber.log.Timber
import javax.inject.Inject
class MessageInfoPresenter @Inject constructor(
private val view: MessageInfoView,
private val strategy: CancelStrategy,
private val mapper: ViewModelMapper,
serverInteractor: GetCurrentServerInteractor,
factory: ConnectionManagerFactory
) {
private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer)
private val client = manager.client
fun loadReadReceipts(messageId: String) {
launchUI(strategy) {
try {
view.showLoading()
val readReceipts = retryIO(description = "getMessageReadReceipts") {
client.getMessageReadReceipts(messageId = messageId).result
}
view.showReadReceipts(mapper.map(readReceipts))
} catch (ex: RocketChatException) {
Timber.e(ex)
view.showGenericErrorMessage()
} finally {
view.hideLoading()
}
}
}
}
package chat.rocket.android.chatinformation.presentation
import chat.rocket.android.chatinformation.viewmodel.ReadReceiptViewModel
import chat.rocket.android.core.behaviours.LoadingView
interface MessageInfoView : LoadingView {
fun showGenericErrorMessage()
fun showReadReceipts(messageReceipts: List<ReadReceiptViewModel>)
}
package chat.rocket.android.chatinformation.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.chatinformation.ui.MessageInfoFragment.Companion.TAG_MESSAGE_INFO_FRAGMENT
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject
fun Context.messageInformationIntent(messageId: String): Intent {
return Intent(this, MessageInfoActivity::class.java).apply {
putExtra(INTENT_MESSAGE_ID, messageId)
}
}
private const val INTENT_MESSAGE_ID = "message_id"
class MessageInfoActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat_room)
setupToolbar()
val messageId = intent.getStringExtra(INTENT_MESSAGE_ID)
requireNotNull(messageId) { "no message_id provided in Intent extras" }
if (supportFragmentManager.findFragmentByTag(TAG_MESSAGE_INFO_FRAGMENT) == null) {
addFragment(TAG_MESSAGE_INFO_FRAGMENT, R.id.fragment_container) {
newInstance(messageId = messageId)
}
}
}
private fun setupToolbar() {
text_room_name.textContent = getString(R.string.message_information_title)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
toolbar.setNavigationOnClickListener { finishActivity() }
}
private fun finishActivity() {
super.onBackPressed()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
}
}
\ No newline at end of file
package chat.rocket.android.chatinformation.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 chat.rocket.android.R
import chat.rocket.android.chatinformation.adapter.ReadReceiptAdapter
import chat.rocket.android.chatinformation.presentation.MessageInfoPresenter
import chat.rocket.android.chatinformation.presentation.MessageInfoView
import chat.rocket.android.chatinformation.viewmodel.ReadReceiptViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.core.model.ReadReceipt
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_message_info.*
import javax.inject.Inject
fun newInstance(messageId: String): Fragment {
return MessageInfoFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_MESSAGE_ID, messageId)
}
}
}
private const val BUNDLE_MESSAGE_ID = "message_id"
class MessageInfoFragment : Fragment(), MessageInfoView {
@Inject
lateinit var presenter: MessageInfoPresenter
private lateinit var adapter: ReadReceiptAdapter
private lateinit var endlessRecyclerViewScrollListener: EndlessRecyclerViewScrollListener
private lateinit var messageId: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
setHasOptionsMenu(true)
val bundle = arguments
if (bundle != null) {
messageId = bundle.getString(BUNDLE_MESSAGE_ID)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_message_info, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
presenter.loadReadReceipts(messageId = messageId)
}
override fun onDestroyView() {
super.onDestroyView()
receipt_list.removeOnScrollListener(endlessRecyclerViewScrollListener)
}
private fun setupRecyclerView() {
// Initialize the endlessRecyclerViewScrollListener so we don't NPE at onDestroyView
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
adapter = ReadReceiptAdapter()
linearLayoutManager.stackFromEnd = true
receipt_list.layoutManager = linearLayoutManager
receipt_list.itemAnimator = DefaultItemAnimator()
receipt_list.adapter = adapter
endlessRecyclerViewScrollListener = object :
EndlessRecyclerViewScrollListener(receipt_list.layoutManager as LinearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
}
}
}
override fun showGenericErrorMessage() {
showToast(R.string.msg_generic_error)
}
override fun showLoading() {
view_loading.setVisible(true)
view_loading.show()
}
override fun hideLoading() {
view_loading.hide()
view_loading.setVisible(false)
}
override fun showReadReceipts(messageReceipts: List<ReadReceiptViewModel>) {
adapter.addAll(messageReceipts)
}
companion object {
const val TAG_MESSAGE_INFO_FRAGMENT = "MessageInfoFragment"
}
}
package chat.rocket.android.chatinformation.viewmodel
data class ReadReceiptViewModel(
val avatar: String,
val name: String,
val time: String
)
\ No newline at end of file
......@@ -77,20 +77,24 @@ abstract class BaseViewHolder<T : BaseViewModel<*>>(
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_message_unpin }?.apply {
setTitle(if (it.pinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = it.pinned
}
data?.let { vm ->
vm.message.let {
val menuItems = view.context.inflate(R.menu.message_actions).toList()
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
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 = menuItems.filterNot {
vm.menuItemsToHide.contains(it.itemId)
}, callback = this@BaseViewHolder)
BottomSheetMenu(adapter).show(view.context)
}
val adapter = ActionListAdapter(menuItems, this@BaseViewHolder)
BottomSheetMenu(adapter).show(view.context)
}
}
}
......
......@@ -5,7 +5,19 @@ 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.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.util.extensions.inflate
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.Message
......@@ -203,6 +215,9 @@ class ChatRoomAdapter(
override fun onActionSelected(item: MenuItem, message: Message) {
message.apply {
when (item.itemId) {
R.id.action_message_info -> {
presenter?.messageInfo(id)
}
R.id.action_message_reply -> {
if (roomName != null && roomType != null) {
presenter?.citeMessage(roomName, roomType, id, true)
......
......@@ -4,6 +4,7 @@ import android.graphics.Color
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
......@@ -40,6 +41,18 @@ class MessageViewHolder(
text_edit_indicator.isVisible = !it.isSystemMessage() && it.editedBy != null
image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false
}
if (data.unread == null) {
read_receipt_view.setVisible(false)
} else {
read_receipt_view.setImageResource(
if (data.unread == true) {
R.drawable.ic_check_unread_24dp
} else {
R.drawable.ic_check_read_24dp
}
)
read_receipt_view.setVisible(true)
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R
import chat.rocket.android.chatinformation.ui.messageInformationIntent
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.server.ui.changeServerIntent
......@@ -49,4 +50,9 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
fun toMessageInformation(messageId: String) {
activity.startActivity(activity.messageInformationIntent(messageId = messageId))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
}
\ No newline at end of file
......@@ -223,7 +223,8 @@ class ChatRoomPresenter @Inject constructor(
type = null,
updatedAt = null,
urls = null,
isTemporary = true
isTemporary = true,
unread = true
)
try {
messagesRepository.save(newMessage)
......@@ -850,4 +851,10 @@ class ChatRoomPresenter @Inject constructor(
}
}
}
fun messageInfo(messageId: String) {
launchUI(strategy) {
navigator.toMessageInformation(messageId = messageId)
}
}
}
\ No newline at end of file
......@@ -146,4 +146,4 @@ interface ChatRoomView : LoadingView, MessageView {
* to reply.
*/
fun openDirectMessage(chatRoom: ChatRoom, permalink: String)
}
\ No newline at end of file
}
......@@ -188,6 +188,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
super.onDestroyView()
}
override fun onResume() {
super.onResume()
activity?.invalidateOptionsMenu()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == REQUEST_CODE_FOR_PERFORM_SAF && resultCode == Activity.RESULT_OK) {
if (resultData != null) {
......@@ -199,6 +204,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.chatroom_actions, menu)
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.action_members_list)?.isVisible = !isBroadcastChannel
}
......
......@@ -14,7 +14,9 @@ data class AudioAttachmentViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
......
......@@ -16,7 +16,9 @@ data class AuthorAttachmentViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
) : BaseAttachmentViewModel<AuthorAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUTHOR_ATTACHMENT.viewType
......
......@@ -13,6 +13,8 @@ interface BaseViewModel<out T> {
var nextDownStreamMessage: BaseViewModel<*>?
var preview: Message?
var isTemporary: Boolean
var unread: Boolean?
var menuItemsToHide: MutableList<Int>
enum class ViewType(val viewType: Int) {
MESSAGE(0),
......
......@@ -15,7 +15,9 @@ data class ColorAttachmentViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
) : BaseAttachmentViewModel<ColorAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.COLOR_ATTACHMENT.viewType
......
......@@ -15,7 +15,9 @@ data class GenericFileAttachmentViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
) : BaseFileAttachmentViewModel<GenericFileAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.GENERIC_FILE_ATTACHMENT.viewType
......
......@@ -14,7 +14,9 @@ data class ImageAttachmentViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
......@@ -15,7 +15,9 @@ data class MessageAttachmentViewModel(
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
) : BaseViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
......
......@@ -11,7 +11,9 @@ data class MessageReplyViewModel(
override var nextDownStreamMessage: BaseViewModel<*>?,
override var preview: Message?,
override var isTemporary: Boolean = false,
override val message: Message
override val message: Message,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
) : BaseViewModel<MessageReply> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_REPLY.viewType
......
......@@ -16,7 +16,9 @@ data class MessageViewModel(
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
var isFirstUnread: Boolean,
override var isTemporary: Boolean = false
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
) : BaseMessageViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType
......
......@@ -15,7 +15,9 @@ data class UrlPreviewViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
) : BaseViewModel<Url> {
override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
......@@ -14,7 +14,9 @@ data class VideoAttachmentViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
......@@ -13,16 +13,21 @@ import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.text.scale
import chat.rocket.android.R
import chat.rocket.android.chatinformation.viewmodel.ReadReceiptViewModel
import chat.rocket.android.chatroom.domain.MessageReply
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.helper.MessageHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository
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.UsersRepository
import chat.rocket.android.server.domain.baseUrl
import chat.rocket.android.server.domain.messageReadReceiptEnabled
import chat.rocket.android.server.domain.messageReadReceiptStoreUsers
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.isNotNullNorEmpty
......@@ -30,6 +35,7 @@ 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.ReadReceipt
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.AudioAttachment
......@@ -53,7 +59,9 @@ class ViewModelMapper @Inject constructor(
private val context: Context,
private val parser: MessageParser,
private val roomsInteractor: ChatRoomsInteractor,
private val usersRepository: UsersRepository,
private val messageHelper: MessageHelper,
private val userHelper: UserHelper,
tokenRepository: TokenRepository,
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor,
......@@ -91,6 +99,23 @@ class ViewModelMapper @Inject constructor(
return@withContext list
}
suspend fun map(
readReceipts: List<ReadReceipt>
): List<ReadReceiptViewModel> = withContext(CommonPool) {
val list = arrayListOf<ReadReceiptViewModel>()
readReceipts.forEach {
list.add(
ReadReceiptViewModel(
avatar = baseUrl.avatarUrl(it.user.username ?: ""),
name = userHelper.displayName(it.user),
time = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(it.timestamp))
)
)
}
return@withContext list
}
private suspend fun translate(
message: Message,
roomViewModel: RoomViewModel
......@@ -118,6 +143,7 @@ class ViewModelMapper @Inject constructor(
for (i in list.size - 1 downTo 0) {
val next = if (i - 1 < 0) null else list[i - 1]
list[i].nextDownStreamMessage = next
mapVisibleActions(list[i])
}
if (isBroadcastReplyAvailable(roomViewModel, message)) {
......@@ -131,6 +157,12 @@ class ViewModelMapper @Inject constructor(
return@withContext list
}
private fun mapVisibleActions(viewModel: BaseViewModel<*>) {
if (!settings.messageReadReceiptStoreUsers()) {
viewModel.menuItemsToHide.add(R.id.action_message_info)
}
}
private suspend fun translateAsNotReversed(
message: Message,
roomViewModel: RoomViewModel
......@@ -184,8 +216,8 @@ class ViewModelMapper @Inject constructor(
private fun isBroadcastReplyAvailable(roomViewModel: RoomViewModel, message: Message): Boolean {
val senderUsername = message.sender?.username
return roomViewModel.isRoom && roomViewModel.isBroadcast &&
!message.isSystemMessage() &&
senderUsername != currentUsername
!message.isSystemMessage() &&
senderUsername != currentUsername
}
private fun mapMessageReply(message: Message, chatRoom: ChatRoom): MessageReplyViewModel {
......@@ -234,7 +266,7 @@ class ViewModelMapper @Inject constructor(
ColorAttachmentViewModel(attachmentUrl = url, id = id, color = color.color,
text = text, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message))
preview = message.copy(message = content.message), unread = message.unread)
}
}
......@@ -262,7 +294,7 @@ class ViewModelMapper @Inject constructor(
AuthorAttachmentViewModel(attachmentUrl = url, id = id, name = authorName,
icon = authorIcon, fields = fieldsText, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message))
preview = message.copy(message = content.message), unread = message.unread)
}
}
......@@ -280,7 +312,7 @@ class ViewModelMapper @Inject constructor(
return MessageAttachmentViewModel(message = content, rawData = message,
messageId = message.id, time = time, senderName = attachmentAuthor,
content = attachmentText, isPinned = message.pinned, reactions = getReactions(message),
preview = message.copy(message = content.message))
preview = message.copy(message = content.message), unread = message.unread)
}
private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseViewModel<*>? {
......@@ -345,12 +377,17 @@ class ViewModelMapper @Inject constructor(
val avatar = getUserAvatar(message)
val preview = mapMessagePreview(message)
val isTemp = message.isTemporary ?: false
val unread = if (settings.messageReadReceiptEnabled()) {
message.unread ?: false
} else {
null
}
val content = getContent(stripMessageQuotes(message))
MessageViewModel(message = stripMessageQuotes(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, reactions = getReactions(message),
isFirstUnread = false, preview = preview, isTemporary = isTemp)
isFirstUnread = false, preview = preview, isTemporary = isTemp, unread = unread)
}
private fun mapMessagePreview(message: Message): Message {
......@@ -413,7 +450,7 @@ class ViewModelMapper @Inject constructor(
}
val username = message.sender?.username ?: "?"
return baseUrl?.let {
return baseUrl.let {
baseUrl.avatarUrl(username)
}
}
......
......@@ -8,6 +8,8 @@ import chat.rocket.android.authentication.server.di.ServerFragmentProvider
import chat.rocket.android.authentication.signup.di.SignupFragmentProvider
import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatinformation.di.MessageInfoFragmentProvider
import chat.rocket.android.chatinformation.ui.MessageInfoActivity
import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider
import chat.rocket.android.chatroom.di.ChatRoomModule
import chat.rocket.android.chatroom.di.FavoriteMessagesFragmentProvider
......@@ -72,4 +74,8 @@ abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(modules = [ChangeServerModule::class])
abstract fun bindChangeServerActivity(): ChangeServerActivity
}
\ No newline at end of file
@PerActivity
@ContributesAndroidInjector(modules = [MessageInfoFragmentProvider::class])
abstract fun bindMessageInfoActiviy(): MessageInfoActivity
}
......@@ -5,6 +5,7 @@ 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.SimpleUser
import chat.rocket.common.model.User
import javax.inject.Inject
......@@ -26,6 +27,10 @@ class UserHelper @Inject constructor(
return if (settings.useRealName()) user.name ?: user.username else user.username
}
fun displayName(user: SimpleUser): String {
return if (settings.useRealName()) user.name ?: user.username ?: "" else user.username ?: ""
}
/**
* Return current logged user's display name.
*
......
......@@ -25,7 +25,7 @@ class RefreshSettingsInteractor @Inject constructor(
HIDE_USER_JOIN, HIDE_USER_LEAVE,
HIDE_TYPE_AU, HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ALLOW_MESSAGE_DELETING,
ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, ALLOW_MESSAGE_STARRING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS,
WIDE_TILE_310, STORE_LAST_MESSAGE)
WIDE_TILE_310, STORE_LAST_MESSAGE, MESSAGE_READ_RECEIPT_ENABLED, MESSAGE_READ_RECEIPT_STORE_USERS)
suspend fun refresh(server: String) {
withContext(CommonPool) {
......
......@@ -46,6 +46,8 @@ 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"
const val MESSAGE_READ_RECEIPT_ENABLED = "Message_Read_Receipt_Enabled"
const val MESSAGE_READ_RECEIPT_STORE_USERS = "Message_Read_Receipt_Store_Users"
/*
* Extension functions for Public Settings.
......@@ -86,6 +88,9 @@ fun PublicSettings.allowedMessageDeleting(): Boolean = this[ALLOW_MESSAGE_DELETI
fun PublicSettings.hasShowLastMessage(): Boolean = this[STORE_LAST_MESSAGE] != null
fun PublicSettings.showLastMessage(): Boolean = this[STORE_LAST_MESSAGE]?.value == true
fun PublicSettings.messageReadReceiptEnabled(): Boolean = this[MESSAGE_READ_RECEIPT_ENABLED]?.value == true
fun PublicSettings.messageReadReceiptStoreUsers(): Boolean = this[MESSAGE_READ_RECEIPT_STORE_USERS]?.value == true
fun PublicSettings.uploadMimeTypeFilter(): Array<String>? {
val values = this[UPLOAD_WHITELIST_MIMETYPES]?.value as String?
if (!values.isNullOrBlank()) {
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/actionMenuColor"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="@color/actionMenuColor"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#1D74F5"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF1D74F5"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#999999"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF999999"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>
<?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:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".chatinformation.ui.MessageInfoActivity">
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:indicatorColor="@color/colorBlack"
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" />
<TextView
android:id="@+id/text_read_by"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textSize="18sp"
android:text="@string/read_by"
app:layout_constraintBottom_toTopOf="@+id/receipt_list"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.v7.widget.RecyclerView
android:id="@+id/receipt_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:scrollbars="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_read_by" />
</android.support.constraint.ConstraintLayout>
......@@ -19,8 +19,8 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
......@@ -62,6 +62,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/layout_avatar"
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif">
......@@ -100,6 +101,14 @@
android:src="@drawable/ic_action_message_star_24dp"
android:visibility="gone"
tools:visibility="visible" />
<ImageView
android:id="@+id/read_receipt_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_check_unread_24dp"
tools:visibility="visible" />
</LinearLayout>
<TextView
......@@ -109,10 +118,10 @@
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginTop="5dp"
app:layout_constraintStart_toStartOf="@+id/top_container"
android:textDirection="locale"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/top_container"
app:layout_constraintTop_toBottomOf="@+id/top_container"
android:textDirection="locale"
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
......
<?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="wrap_content"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/receipt_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/receipt_time"
app:layout_constraintStart_toEndOf="@+id/avatar_layout"
app:layout_constraintTop_toTopOf="parent"
tools:text="John Doe" />
<include
android:id="@+id/avatar_layout"
layout="@layout/avatar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/receipt_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:gravity="center"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="04/06/2018 14:18:36" />
</android.support.constraint.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_message_info"
android:icon="@drawable/ic_action_message_info_outline_24dp"
android:title="@string/action_msg_info" />
<item
android:id="@+id/action_message_reply"
android:icon="@drawable/ic_action_message_reply_24dp"
......
......@@ -143,6 +143,7 @@
<!-- Message actions -->
<string name="action_msg_reply">Respuesta</string>
<string name="action_msg_info">Información del mensaje</string>
<string name="action_msg_edit">Editar</string>
<string name="action_msg_copy">Copiar</string>
<string name="action_msg_quote">Citar</string>
......@@ -245,4 +246,6 @@
<string name="notif_action_reply_hint">RESPUESTA</string>
<string name="notif_error_sending">La respuesta ha fallado. Inténtalo de nuevo.</string>
<string name="notif_success_sending">Mensaje enviado a %1$s!</string>
<string name="read_by">Leído por</string>
<string name="message_information_title">Información del mensaje</string>
</resources>
......@@ -142,6 +142,7 @@
<!-- Message actions -->
<string name="action_msg_reply">Répondre</string>
<string name="action_msg_info">Informations sur le message</string>
<string name="action_msg_edit">Modifier</string>
<string name="action_msg_copy">Copier</string>
<string name="action_msg_quote">Citation</string>
......@@ -245,4 +246,6 @@
<string name="notif_action_reply_hint">RÉPONDRE</string>
<string name="notif_error_sending">La réponse a échoué. Veuillez réessayer.</string>
<string name="notif_success_sending">Message envoyé à %1$s!</string>
<string name="read_by">Lire par</string>
<string name="message_information_title">Informations sur le message</string>
</resources>
......@@ -144,6 +144,7 @@
<!-- Message actions -->
<string name="action_msg_reply">जवाब दें</string>
<string name="action_msg_info">संदेश जानकारी</string>
<string name="action_msg_edit">संपादन करें</string>
<string name="action_msg_copy">कॉपी</string>
<string name="action_msg_quote">उद्धरण</string>
......@@ -246,4 +247,6 @@
<string name="notif_action_reply_hint">जवाब</string>
<string name="notif_error_sending">उत्तर विफल हुआ है। कृपया फिर से प्रयास करें।</string>
<string name="notif_success_sending">संदेश भेजा गया %1$s!</string>
<string name="read_by">द्वारा पढ़ें</string>
<string name="message_information_title">संदेश की जानकारी</string>
</resources>
\ No newline at end of file
......@@ -133,6 +133,7 @@
<!-- Message actions -->
<string name="action_msg_reply">Responder</string>
<string name="action_msg_info">Informações da mensagem</string>
<string name="action_msg_edit">Editar</string>
<string name="action_msg_copy">Copiar</string>
<string name="action_msg_quote">Citar</string>
......@@ -231,4 +232,6 @@
<string name="notif_action_reply_hint">RESPONDER</string>
<string name="notif_error_sending">Falha ao enviar a mensagem.</string>
<string name="notif_success_sending">Mensagem enviada para %1$s!</string>
<string name="read_by">Lida por</string>
<string name="message_information_title">Informações da mensagem</string>
</resources>
......@@ -132,6 +132,7 @@
<!-- Message actions -->
<string name="action_msg_reply">Reply</string>
<string name="action_msg_info">Message info</string>
<string name="action_msg_edit">Edit</string>
<string name="action_msg_copy">Copy</string>
<string name="action_msg_quote">Quote</string>
......@@ -230,4 +231,6 @@
<string name="notif_action_reply_hint">REPLY</string>
<string name="notif_error_sending">Reply has failed. Please try again.</string>
<string name="notif_success_sending">Message sent to %1$s!</string>
<string name="read_by">Read by</string>
<string name="message_information_title">Message information</string>
</resources>
\ 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