BayernMessenger/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt

526 lines
22 KiB
Kotlin
Raw Normal View History

2019-01-18 10:12:08 +00:00
/*
* Copyright 2019 New Vector Ltd
2019-01-18 10:12:08 +00:00
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
2019-01-18 10:12:08 +00:00
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
2019-01-18 10:12:08 +00:00
*/
package im.vector.riotredesign.features.home.room.detail
2018-10-19 13:30:40 +00:00
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
2019-05-07 12:02:15 +00:00
import android.net.Uri
2018-10-19 13:30:40 +00:00
import android.os.Bundle
2018-12-29 16:54:03 +00:00
import android.os.Parcelable
import android.text.Editable
2019-04-09 07:58:07 +00:00
import android.text.Spannable
2019-05-07 12:02:15 +00:00
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
2018-10-19 13:30:40 +00:00
import android.view.View
2019-05-07 12:02:15 +00:00
import android.widget.TextView
import android.widget.Toast
2019-04-09 11:36:33 +00:00
import androidx.appcompat.app.AlertDialog
2019-05-07 12:02:15 +00:00
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
2019-05-07 12:02:15 +00:00
import com.airbnb.mvrx.MvRx
2018-12-29 16:54:03 +00:00
import com.airbnb.mvrx.fragmentViewModel
2019-05-07 12:02:15 +00:00
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar
2019-04-17 10:50:43 +00:00
import com.jaiselrahman.filepicker.activity.FilePickerActivity
import com.jaiselrahman.filepicker.config.Configurations
import com.jaiselrahman.filepicker.model.MediaFile
import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy
2019-04-09 07:58:07 +00:00
import im.vector.matrix.android.api.session.Session
2019-05-08 13:49:32 +00:00
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
2018-10-19 13:30:40 +00:00
import im.vector.riotredesign.R
2019-04-02 13:59:36 +00:00
import im.vector.riotredesign.core.dialogs.DialogListItem
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
2019-04-09 11:36:33 +00:00
import im.vector.riotredesign.core.extensions.observeEvent
2019-04-09 07:58:07 +00:00
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.platform.ToolbarConfigurable
2019-04-05 08:40:59 +00:00
import im.vector.riotredesign.core.platform.VectorBaseFragment
2019-04-17 10:35:18 +00:00
import im.vector.riotredesign.core.utils.*
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
2019-04-09 07:58:07 +00:00
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotredesign.features.command.Command
2018-11-02 08:12:26 +00:00
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomePermalinkHandler
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
2019-05-07 12:02:15 +00:00
import im.vector.riotredesign.features.home.room.detail.timeline.action.ActionsHandler
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageMenuViewModel
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
2019-05-07 12:02:15 +00:00
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
2019-04-09 07:58:07 +00:00
import im.vector.riotredesign.features.html.PillImageSpan
import im.vector.riotredesign.features.media.ImageContentRenderer
import im.vector.riotredesign.features.media.ImageMediaViewerActivity
import im.vector.riotredesign.features.media.VideoContentRenderer
import im.vector.riotredesign.features.media.VideoMediaViewerActivity
2018-12-29 16:54:03 +00:00
import kotlinx.android.parcel.Parcelize
2018-10-21 18:27:50 +00:00
import kotlinx.android.synthetic.main.fragment_room_detail.*
2018-10-19 13:30:40 +00:00
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
import org.koin.core.parameter.parametersOf
import timber.log.Timber
2019-05-07 12:02:15 +00:00
import java.io.File
2018-10-19 13:30:40 +00:00
2018-12-29 16:54:03 +00:00
@Parcelize
data class RoomDetailArgs(
val roomId: String,
val eventId: String? = null
) : Parcelable
private const val CAMERA_VALUE_TITLE = "attachment"
private const val REQUEST_FILES_REQUEST_CODE = 0
private const val TAKE_IMAGE_REQUEST_CODE = 1
2019-05-07 12:02:15 +00:00
class RoomDetailFragment :
VectorBaseFragment(),
TimelineEventController.Callback,
AutocompleteUserPresenter.Callback {
2018-10-19 13:30:40 +00:00
companion object {
2018-12-29 16:54:03 +00:00
fun newInstance(args: RoomDetailArgs): RoomDetailFragment {
2018-10-19 13:30:40 +00:00
return RoomDetailFragment().apply {
2018-12-29 16:54:03 +00:00
setArguments(args)
2018-10-19 13:30:40 +00:00
}
}
}
2019-04-09 07:58:07 +00:00
private val session by inject<Session>()
private val glideRequests by lazy {
GlideApp.with(this)
}
2018-12-29 16:54:03 +00:00
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) }
private val autocompleteUserPresenter: AutocompleteUserPresenter by inject { parametersOf(this) }
private val homePermalinkHandler: HomePermalinkHandler by inject()
2018-11-25 15:17:13 +00:00
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
2018-10-19 13:30:40 +00:00
2019-04-02 10:14:16 +00:00
override fun getLayoutResId() = R.layout.fragment_room_detail
2018-10-19 13:30:40 +00:00
2019-05-07 12:02:15 +00:00
lateinit var actionViewModel: ActionsHandler
2018-10-19 13:30:40 +00:00
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
2019-05-07 12:02:15 +00:00
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
setupRecyclerView()
setupToolbar()
setupComposer()
2019-04-02 13:59:36 +00:00
setupAttachmentButton()
2018-12-29 16:54:03 +00:00
roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) }
2019-04-09 11:36:33 +00:00
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
2019-05-07 12:02:15 +00:00
actionViewModel.actionCommandEvent.observe(this, Observer {
handleActions(it)
})
2018-10-19 13:30:40 +00:00
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK && data != null) {
when (requestCode) {
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
}
}
}
override fun onResume() {
super.onResume()
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
}
// PRIVATE METHODS *****************************************************************************
private fun setupToolbar() {
2019-04-05 08:40:59 +00:00
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
parentActivity.configure(toolbar)
}
2018-10-19 13:30:40 +00:00
}
private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
2018-10-21 18:27:50 +00:00
recyclerView.layoutManager = layoutManager
recyclerView.itemAnimator = null
recyclerView.setHasFixedSize(true)
timelineEventController.addModelBuildListener {
it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback)
}
recyclerView.addOnScrollListener(
EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
roomDetailViewModel.process(RoomDetailActions.LoadMore(direction))
})
recyclerView.setController(timelineEventController)
timelineEventController.callback = this
2018-10-21 18:27:50 +00:00
}
private fun setupComposer() {
val elevation = 6f
val backgroundDrawable = ColorDrawable(Color.WHITE)
Autocomplete.on<Command>(composerEditText)
2019-04-09 07:58:07 +00:00
.with(CommandAutocompletePolicy())
.with(autocompleteCommandPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<Command> {
2019-04-09 07:58:07 +00:00
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
editable.clear()
editable
2019-04-09 07:58:07 +00:00
.append(item.command)
.append(" ")
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
autocompleteUserPresenter.callback = this
Autocomplete.on<User>(composerEditText)
2019-04-09 07:58:07 +00:00
.with(CharPolicy('@', true))
.with(autocompleteUserPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<User> {
2019-04-09 07:58:07 +00:00
override fun onPopupItemClicked(editable: Editable, item: User): Boolean {
// Detect last '@' and remove it
var startIndex = editable.lastIndexOf("@")
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
val displayName = item.displayName ?: item.userId
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
val user = session.getUser(item.userId)
val span = PillImageSpan(glideRequests, context!!, item.userId, user)
2019-04-09 11:36:33 +00:00
span.bind(composerEditText)
2019-04-09 07:58:07 +00:00
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
sendButton.setOnClickListener {
val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) {
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
}
}
}
2019-04-02 13:59:36 +00:00
private fun setupAttachmentButton() {
attachmentButton.setOnClickListener {
val intent = Intent(requireContext(), FilePickerActivity::class.java)
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
.setCheckPermission(true)
.setShowFiles(true)
.setShowAudios(true)
.setSkipZeroSizeFiles(true)
.build())
startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE)
/*
2019-04-02 13:59:36 +00:00
val items = ArrayList<DialogListItem>()
// Send file
items.add(DialogListItem.SendFile)
// Send voice
2019-04-02 13:59:36 +00:00
if (PreferencesManager.isSendVoiceFeatureEnabled(this)) {
items.add(DialogListItem.SendVoice.INSTANCE)
}
2019-04-02 13:59:36 +00:00
// Send sticker
//items.add(DialogListItem.SendSticker)
2019-04-02 13:59:36 +00:00
// Camera
//if (PreferencesManager.useNativeCamera(this)) {
items.add(DialogListItem.TakePhoto)
items.add(DialogListItem.TakeVideo)
//} else {
// items.add(DialogListItem.TakePhotoVideo.INSTANCE)
2019-04-02 13:59:36 +00:00
// }
val adapter = DialogSendItemAdapter(requireContext(), items)
AlertDialog.Builder(requireContext())
.setAdapter(adapter) { _, position ->
onSendChoiceClicked(items[position])
}
2019-04-02 13:59:36 +00:00
.setNegativeButton(R.string.cancel, null)
.show()
*/
2019-04-02 13:59:36 +00:00
}
}
private fun onSendChoiceClicked(dialogListItem: DialogListItem) {
Timber.v("On send choice clicked: $dialogListItem")
2019-04-02 13:59:36 +00:00
when (dialogListItem) {
2019-04-17 10:35:18 +00:00
is DialogListItem.SendFile -> {
// launchFileIntent
2019-04-02 13:59:36 +00:00
}
2019-04-17 10:35:18 +00:00
is DialogListItem.SendVoice -> {
2019-04-02 13:59:36 +00:00
//launchAudioRecorderIntent()
}
2019-04-17 10:35:18 +00:00
is DialogListItem.SendSticker -> {
2019-04-02 13:59:36 +00:00
//startStickerPickerActivity()
}
2019-04-17 10:35:18 +00:00
is DialogListItem.TakePhotoVideo ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
// launchCamera()
}
is DialogListItem.TakePhoto ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) {
openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE)
}
is DialogListItem.TakeVideo ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) {
// launchNativeVideoRecorder()
}
2019-04-02 13:59:36 +00:00
}
}
private fun handleMediaIntent(data: Intent) {
val files: ArrayList<MediaFile> = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES)
roomDetailViewModel.process(RoomDetailActions.SendMedia(files))
}
2018-12-29 16:54:03 +00:00
private fun renderState(state: RoomDetailViewState) {
renderRoomSummary(state)
timelineEventController.setTimeline(state.timeline)
2018-12-29 16:54:03 +00:00
}
private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let {
2018-10-29 16:20:08 +00:00
toolbarTitleView.text = it.displayName
2018-11-02 08:12:26 +00:00
AvatarRenderer.render(it, toolbarAvatarImageView)
2018-10-29 16:20:08 +00:00
if (it.topic.isNotEmpty()) {
toolbarSubtitleView.visibility = View.VISIBLE
toolbarSubtitleView.text = it.topic
} else {
toolbarSubtitleView.visibility = View.GONE
}
}
}
private fun renderTextComposerState(state: TextComposerViewState) {
autocompleteUserPresenter.render(state.asyncUsers)
}
2019-04-09 11:36:33 +00:00
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
when (sendMessageResult) {
2019-04-09 12:35:18 +00:00
is SendMessageResult.MessageSent,
2019-04-17 10:35:18 +00:00
is SendMessageResult.SlashCommandHandled -> {
2019-04-09 11:36:33 +00:00
// Clear composer
composerEditText.text = null
}
2019-04-17 10:35:18 +00:00
is SendMessageResult.SlashCommandError -> {
2019-04-09 12:35:18 +00:00
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
2019-04-09 11:36:33 +00:00
}
2019-04-17 10:35:18 +00:00
is SendMessageResult.SlashCommandUnknown -> {
2019-04-09 12:35:18 +00:00
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
}
2019-04-17 10:35:18 +00:00
is SendMessageResult.SlashCommandResultOk -> {
2019-04-09 12:35:18 +00:00
// Ignore
}
2019-04-17 10:35:18 +00:00
is SendMessageResult.SlashCommandResultError -> {
2019-04-09 12:35:18 +00:00
displayCommandError(sendMessageResult.throwable.localizedMessage)
2019-04-09 11:36:33 +00:00
}
is SendMessageResult.SlashCommandNotImplemented -> {
2019-04-09 12:35:18 +00:00
displayCommandError(getString(R.string.not_implemented))
2019-04-09 11:36:33 +00:00
}
}
}
2019-04-09 12:35:18 +00:00
private fun displayCommandError(message: String) {
2019-04-09 11:36:33 +00:00
AlertDialog.Builder(activity!!)
.setTitle(R.string.command_error)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
2019-04-12 10:38:02 +00:00
// TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String) {
homePermalinkHandler.launch(url)
}
override fun onEventVisible(event: TimelineEvent) {
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
}
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent)
}
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent)
}
2019-04-12 10:38:02 +00:00
override fun onFileMessageClicked(messageFileContent: MessageFileContent) {
vectorBaseActivity.notImplemented()
}
override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) {
vectorBaseActivity.notImplemented()
}
2019-05-08 13:49:32 +00:00
override fun onEventCellClicked(eventId: String, informationData: MessageInformationData, messageContent: MessageContent, view: View) {
val roomId = (arguments?.get(MvRx.KEY_ARG) as? RoomDetailArgs)?.roomId
if (roomId.isNullOrBlank()) {
Timber.e("Missing RoomId, cannot open bottomsheet")
return
}
MessageActionsBottomSheet
.newInstance(eventId, roomId, informationData)
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
}
2019-04-12 10:38:02 +00:00
// AutocompleteUserPresenter.Callback
2019-05-07 12:02:15 +00:00
override fun onEventLongClicked(eventId: String, informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val roomId = (arguments?.get(MvRx.KEY_ARG) as? RoomDetailArgs)?.roomId
if (roomId.isNullOrBlank()) {
Timber.e("Missing RoomId, cannot open bottomsheet")
return false
}
MessageActionsBottomSheet
.newInstance(eventId, roomId, informationData)
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
return true
}
2019-05-08 13:49:32 +00:00
override fun onAvatarClicked(informationData: MessageInformationData) {
vectorBaseActivity.notImplemented()
}
2019-05-07 12:02:15 +00:00
// AutocompleteUserPresenter.Callback
override fun onQueryUsers(query: CharSequence?) {
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
}
2019-05-07 12:02:15 +00:00
private fun handleActions(it: LiveEvent<ActionsHandler.ActionData>?) {
it?.getContentIfNotHandled()?.let { actionData ->
when (actionData.actionId) {
MessageMenuViewModel.ACTION_ADD_REACTION -> {
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext()), 0)
}
MessageMenuViewModel.ACTION_COPY -> {
//I need info about the current selected message :/
copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false)
val snack = Snackbar.make(view!!, requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
snack.show()
}
MessageMenuViewModel.ACTION_SHARE -> {
//TODO current data communication is too limited
//Need to now the media type
actionData.data?.toString()?.let {
//TODO bad, just POC
BigImageViewer.imageLoader().loadImage(
actionData.hashCode(),
Uri.parse(it),
object : ImageLoader.Callback {
override fun onFinish() {}
override fun onSuccess(image: File?) {
if (image != null)
shareMedia(requireContext(), image!!, "image/*")
}
override fun onFail(error: Exception?) {}
override fun onCacheHit(imageType: Int, image: File?) {}
override fun onCacheMiss(imageType: Int, image: File?) {}
override fun onProgress(progress: Int) {}
override fun onStart() {}
}
)
}
}
MessageMenuViewModel.VIEW_SOURCE,
MessageMenuViewModel.VIEW_DECRYPTED_SOURCE -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
it.text = actionData.data?.toString() ?: ""
}
AlertDialog.Builder(requireActivity())
.setView(view)
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
.show()
}
else -> {
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
}
}
}
}
2018-11-02 09:17:37 +00:00
}