From 7e29665fd0d5d8c3ebaf3745d4e77428390b53b1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Sep 2019 18:33:57 +0200 Subject: [PATCH] Timeline: add some comments and checks --- .../api/session/room/timeline/Timeline.kt | 26 +- .../session/room/timeline/DefaultTimeline.kt | 3 + .../room/timeline/TimelineHiddenReadMarker.kt | 5 +- .../timeline/TimelineHiddenReadReceipts.kt | 3 + .../home/room/detail/RoomDetailFragment.kt | 1140 ++++++++--------- .../ScrollOnHighlightedEventCallback.kt | 14 + 6 files changed, 619 insertions(+), 572 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 13eca813c7..9873b75e70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -44,6 +44,10 @@ interface Timeline { */ fun dispose() + /** + * This method restarts the timeline, erases all built events and pagination states. + * It then loads events around the eventId. If eventId is null, it does restart the live timeline. + */ fun restartWithEventId(eventId: String?) @@ -62,19 +66,39 @@ interface Timeline { */ fun paginate(direction: Direction, count: Int) + /** + * Returns the number of sending events + */ fun pendingEventCount(): Int + /** + * Returns the number of failed sending events. + */ fun failedToDeliverEventCount(): Int + /** + * Returns the index of a built event or null. + */ fun getIndexOfEvent(eventId: String?): Int? + /** + * Returns the built [TimelineEvent] at index or null + */ fun getTimelineEventAtIndex(index: Int): TimelineEvent? + /** + * Returns the built [TimelineEvent] with eventId or null + */ fun getTimelineEventWithId(eventId: String?): TimelineEvent? + /** + * Returns the first displayable events starting from eventId. + * It does depend on the provided [TimelineSettings]. + */ fun getFirstDisplayableEventId(eventId: String): String? - interface Listener { + + interface Listener { /** * Call when the timeline has been updated through pagination or sync. * @param snapshot the most uptodate snapshot diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index b3d83a2286..f22be74f70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -120,6 +120,9 @@ internal class DefaultTimeline( private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> + if (!results.isLoaded || !results.isValid) { + return@OrderedRealmCollectionChangeListener + } if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { handleInitialLoad() } else { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt index 532a66140e..7ae6cbcfe1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt @@ -45,6 +45,9 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String) private lateinit var delegate: Delegate private val readMarkerListener = RealmObjectChangeListener { readMarker, _ -> + if (!readMarker.isLoaded || !readMarker.isValid) { + return@RealmObjectChangeListener + } var hasChange = false previousDisplayedEventId?.also { hasChange = delegate.rebuildEvent(it, false) @@ -53,7 +56,7 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String) val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarker.eventId).findFirst() == null if (isEventHidden) { val hiddenEvent = readMarker.timelineEvent?.firstOrNull() - ?: return@RealmObjectChangeListener + ?: return@RealmObjectChangeListener val displayIndex = hiddenEvent.root?.displayIndex if (displayIndex != null) { // Then we are looking for the first displayable event after the hidden one diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 5408668576..f932e6f3c0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -53,6 +53,9 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu private lateinit var delegate: Delegate private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> + if (!collection.isLoaded || !collection.isValid) { + return@OrderedRealmCollectionChangeListener + } var hasChange = false // Deletion here means we don't have any readReceipts for the given hidden events changeSet.deletions.forEach { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index f6d50046a9..72f0005d4c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -507,614 +507,614 @@ class RoomDetailFragment : R.drawable.ic_reply, object : RoomMessageTouchHelperCallback.QuickReplayHandler { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) - } + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED } + else -> false } - }) - val touchHelper = ItemTouchHelper(swipeCallback) - touchHelper.attachToRecyclerView(recyclerView) - } + } + }) + val touchHelper = ItemTouchHelper(swipeCallback) + touchHelper.attachToRecyclerView(recyclerView) } + } - private fun updateJumpToBottomViewVisibility() { - debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { - Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") - if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) { - jumpToBottomView.show() - } else { - jumpToBottomView.hide() - } - }) - } - - private fun setupComposer() { - val elevation = 6f - val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) - Autocomplete.on(composerLayout.composerEditText) - .with(commandAutocompletePolicy) - .with(autocompleteCommandPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { - editable.clear() - editable - .append(item.command) - .append(" ") - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() - - autocompleteUserPresenter.callback = this - Autocomplete.on(composerLayout.composerEditText) - .with(CharPolicy('@', true)) - .with(autocompleteUserPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - 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, avatarRenderer, requireContext(), item.userId, user) - span.bind(composerLayout.composerEditText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() - - composerLayout.sendButton.setOnClickListener { - if (lockSendButton) { - Timber.w("Send button is locked") - return@setOnClickListener - } - val textMessage = composerLayout.composerEditText.text.toString() - if (textMessage.isNotBlank()) { - lockSendButton = true - roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, vectorPreferences.isMarkdownEnabled())) - } - } - composerLayout.composerRelatedMessageCloseButton.setOnClickListener { - roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString())) - } - } - - private fun setupAttachmentButton() { - composerLayout.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) - /* - val items = ArrayList() - // Send file - items.add(DialogListItem.SendFile) - // Send voice - - if (vectorPreferences.isSendVoiceFeatureEnabled()) { - items.add(DialogListItem.SendVoice.INSTANCE) - } - - - // Send sticker - //items.add(DialogListItem.SendSticker) - // Camera - - //if (vectorPreferences.useNativeCamera()) { - items.add(DialogListItem.TakePhoto) - items.add(DialogListItem.TakeVideo) - //} else { - // items.add(DialogListItem.TakePhotoVideo.INSTANCE) - // } - val adapter = DialogSendItemAdapter(requireContext(), items) - AlertDialog.Builder(requireContext()) - .setAdapter(adapter) { _, position -> - onSendChoiceClicked(items[position]) - } - .setNegativeButton(R.string.cancel, null) - .show() - */ - } - } - - private fun setupInviteView() { - inviteView.callback = this - } - - private fun onSendChoiceClicked(dialogListItem: DialogListItem) { - Timber.v("On send choice clicked: $dialogListItem") - when (dialogListItem) { - is DialogListItem.SendFile -> { - // launchFileIntent - } - is DialogListItem.SendVoice -> { - //launchAudioRecorderIntent() - } - is DialogListItem.SendSticker -> { - //startStickerPickerActivity() - } - 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() - } - } - } - - private fun handleMediaIntent(data: Intent) { - val files: ArrayList = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES) - roomDetailViewModel.process(RoomDetailActions.SendMedia(files)) - } - - private fun renderState(state: RoomDetailViewState) { - renderRoomSummary(state) - val summary = state.asyncRoomSummary() - val inviter = state.asyncInviter() - if (summary?.membership == Membership.JOIN) { - scrollOnHighlightedEventCallback.timeline = state.timeline - timelineEventController.update(state) - inviteView.visibility = View.GONE - val uid = session.myUserId - val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) - avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) - - } else if (summary?.membership == Membership.INVITE && inviter != null) { - inviteView.visibility = View.VISIBLE - inviteView.render(inviter, VectorInviteView.Mode.LARGE) - - // Intercept click event - inviteView.setOnClickListener { } - } else if (state.asyncInviter.complete) { - vectorBaseActivity.finish() - } - if (state.tombstoneEvent == null) { - composerLayout.visibility = View.VISIBLE - composerLayout.setRoomEncrypted(state.isEncrypted) - notificationAreaView.render(NotificationAreaView.State.Hidden) + private fun updateJumpToBottomViewVisibility() { + debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { + Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") + if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) { + jumpToBottomView.show() } else { - composerLayout.visibility = View.GONE - notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) + jumpToBottomView.hide() } - jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId) - } + }) + } - private fun renderRoomSummary(state: RoomDetailViewState) { - state.asyncRoomSummary()?.let { - - if (it.membership.isLeft()) { - Timber.w("The room has been left") - activity?.finish() - } else { - roomToolbarTitleView.text = it.displayName - avatarRenderer.render(it, roomToolbarAvatarImageView) - roomToolbarSubtitleView.setTextOrHide(it.topic) - } - - jumpToBottomView.count = it.notificationCount - } - } - - private fun renderTextComposerState(state: TextComposerViewState) { - autocompleteUserPresenter.render(state.asyncUsers) - } - - private fun renderTombstoneEventHandling(async: Async) { - when (async) { - is Loading -> { - // TODO Better handling progress - vectorBaseActivity.showWaitingView() - vectorBaseActivity.waiting_view_status_text.visibility = View.VISIBLE - vectorBaseActivity.waiting_view_status_text.text = getString(R.string.joining_room) - } - is Success -> { - navigator.openRoom(vectorBaseActivity, async()) - vectorBaseActivity.finish() - } - is Fail -> { - vectorBaseActivity.hideWaitingView() - vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) - } - } - } - - private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { - when (sendMessageResult) { - is SendMessageResult.MessageSent -> { - updateComposerText("") - } - is SendMessageResult.SlashCommandHandled -> { - sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } - updateComposerText("") - } - is SendMessageResult.SlashCommandError -> { - displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) - } - is SendMessageResult.SlashCommandUnknown -> { - displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) - } - is SendMessageResult.SlashCommandResultOk -> { - updateComposerText("") - } - is SendMessageResult.SlashCommandResultError -> { - displayCommandError(sendMessageResult.throwable.localizedMessage) - } - is SendMessageResult.SlashCommandNotImplemented -> { - displayCommandError(getString(R.string.not_implemented)) - } - } - - lockSendButton = false - } - - private fun displayCommandError(message: String) { - AlertDialog.Builder(activity!!) - .setTitle(R.string.command_error) - .setMessage(message) - .setPositiveButton(R.string.ok, null) - .show() - } - -// TimelineEventController.Callback ************************************************************ - - override fun onUrlClicked(url: String): Boolean { - return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { - override fun navToRoom(roomId: String, eventId: String?): Boolean { - // Same room? - if (roomId == roomDetailArgs.roomId) { - // Navigation to same room - if (eventId == null) { - showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) - } else { - // Highlight and scroll to this event - roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, true)) - } + private fun setupComposer() { + val elevation = 6f + val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) + Autocomplete.on(composerLayout.composerEditText) + .with(commandAutocompletePolicy) + .with(autocompleteCommandPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { + editable.clear() + editable + .append(item.command) + .append(" ") return true } - // Not handled - return false - } - }) - } - - override fun onUrlLongClicked(url: String): Boolean { - if (url != getString(R.string.edited_suffix)) { - // Copy the url to the clipboard - copyToClipboard(requireContext(), url, true, R.string.link_copied_to_clipboard) - } - return true - } - - override fun onEventVisible(event: TimelineEvent) { - roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsVisible(event)) - } - - override fun onEventInvisible(event: TimelineEvent) { - roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsInvisible(event)) - } - - override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) { - vectorBaseActivity.notImplemented("encrypted message click") - } - - override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { - // TODO Use navigator - - val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) - val pairs = ArrayList>() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - requireActivity().window.decorView.findViewById(android.R.id.statusBarBackground)?.let { - pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) - } - requireActivity().window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { - pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) - } - } - pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) - pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) - pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) - - val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( - requireActivity(), *pairs.toTypedArray()).toBundle() - startActivity(intent, bundle) - } - - override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { - // TODO Use navigator - val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) - startActivity(intent) - } - - override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { - val action = RoomDetailActions.DownloadFile(eventId, messageFileContent) - // We need WRITE_EXTERNAL permission - if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { - roomDetailViewModel.process(action) - } else { - roomDetailViewModel.pendingAction = action - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - if (allGranted(grantResults)) { - if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) { - val action = roomDetailViewModel.pendingAction - - if (action != null) { - roomDetailViewModel.pendingAction = null - roomDetailViewModel.process(action) + override fun onPopupVisibilityChanged(shown: Boolean) { } - } + }) + .build() + + autocompleteUserPresenter.callback = this + Autocomplete.on(composerLayout.composerEditText) + .with(CharPolicy('@', true)) + .with(autocompleteUserPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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, avatarRenderer, requireContext(), item.userId, user) + span.bind(composerLayout.composerEditText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + + composerLayout.sendButton.setOnClickListener { + if (lockSendButton) { + Timber.w("Send button is locked") + return@setOnClickListener + } + val textMessage = composerLayout.composerEditText.text.toString() + if (textMessage.isNotBlank()) { + lockSendButton = true + roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, vectorPreferences.isMarkdownEnabled())) } } - - override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { - vectorBaseActivity.notImplemented("open audio file") + composerLayout.composerRelatedMessageCloseButton.setOnClickListener { + roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString())) } + } - override fun onLoadMore(direction: Timeline.Direction) { - roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) + private fun setupAttachmentButton() { + composerLayout.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) + /* + val items = ArrayList() + // Send file + items.add(DialogListItem.SendFile) + // Send voice + + if (vectorPreferences.isSendVoiceFeatureEnabled()) { + items.add(DialogListItem.SendVoice.INSTANCE) + } + + + // Send sticker + //items.add(DialogListItem.SendSticker) + // Camera + + //if (vectorPreferences.useNativeCamera()) { + items.add(DialogListItem.TakePhoto) + items.add(DialogListItem.TakeVideo) + //} else { + // items.add(DialogListItem.TakePhotoVideo.INSTANCE) + // } + val adapter = DialogSendItemAdapter(requireContext(), items) + AlertDialog.Builder(requireContext()) + .setAdapter(adapter) { _, position -> + onSendChoiceClicked(items[position]) + } + .setNegativeButton(R.string.cancel, null) + .show() + */ } + } - override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { + private fun setupInviteView() { + inviteView.callback = this + } + private fun onSendChoiceClicked(dialogListItem: DialogListItem) { + Timber.v("On send choice clicked: $dialogListItem") + when (dialogListItem) { + is DialogListItem.SendFile -> { + // launchFileIntent + } + is DialogListItem.SendVoice -> { + //launchAudioRecorderIntent() + } + is DialogListItem.SendSticker -> { + //startStickerPickerActivity() + } + 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() + } } + } - override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean { - view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - val roomId = roomDetailArgs.roomId + private fun handleMediaIntent(data: Intent) { + val files: ArrayList = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES) + roomDetailViewModel.process(RoomDetailActions.SendMedia(files)) + } - this.view?.hideKeyboard() - MessageActionsBottomSheet - .newInstance(roomId, informationData) - .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") - return true + private fun renderState(state: RoomDetailViewState) { + renderRoomSummary(state) + val summary = state.asyncRoomSummary() + val inviter = state.asyncInviter() + if (summary?.membership == Membership.JOIN) { + scrollOnHighlightedEventCallback.timeline = state.timeline + timelineEventController.update(state) + inviteView.visibility = View.GONE + val uid = session.myUserId + val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) + avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) + + } else if (summary?.membership == Membership.INVITE && inviter != null) { + inviteView.visibility = View.VISIBLE + inviteView.render(inviter, VectorInviteView.Mode.LARGE) + + // Intercept click event + inviteView.setOnClickListener { } + } else if (state.asyncInviter.complete) { + vectorBaseActivity.finish() } - - override fun onAvatarClicked(informationData: MessageInformationData) { - vectorBaseActivity.notImplemented("Click on user avatar") + if (state.tombstoneEvent == null) { + composerLayout.visibility = View.VISIBLE + composerLayout.setRoomEncrypted(state.isEncrypted) + notificationAreaView.render(NotificationAreaView.State.Hidden) + } else { + composerLayout.visibility = View.GONE + notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } + jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId) + } - @SuppressLint("SetTextI18n") - override fun onMemberNameClicked(informationData: MessageInformationData) { - insertUserDisplayNameInTextEditor(informationData.memberName?.toString()) - } + private fun renderRoomSummary(state: RoomDetailViewState) { + state.asyncRoomSummary()?.let { - override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { - if (on) { - //we should test the current real state of reaction on this event - roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId)) + if (it.membership.isLeft()) { + Timber.w("The room has been left") + activity?.finish() } else { - //I need to redact a reaction - roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction)) + roomToolbarTitleView.text = it.displayName + avatarRenderer.render(it, roomToolbarAvatarImageView) + roomToolbarSubtitleView.setTextOrHide(it.topic) + } + jumpToBottomView.count = it.notificationCount + jumpToBottomView.drawBadge = it.hasUnreadMessages + } + } + + private fun renderTextComposerState(state: TextComposerViewState) { + autocompleteUserPresenter.render(state.asyncUsers) + } + + private fun renderTombstoneEventHandling(async: Async) { + when (async) { + is Loading -> { + // TODO Better handling progress + vectorBaseActivity.showWaitingView() + vectorBaseActivity.waiting_view_status_text.visibility = View.VISIBLE + vectorBaseActivity.waiting_view_status_text.text = getString(R.string.joining_room) + } + is Success -> { + navigator.openRoom(vectorBaseActivity, async()) + vectorBaseActivity.finish() + } + is Fail -> { + vectorBaseActivity.hideWaitingView() + vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) + } + } + } + + private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { + when (sendMessageResult) { + is SendMessageResult.MessageSent -> { + updateComposerText("") + } + is SendMessageResult.SlashCommandHandled -> { + sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } + updateComposerText("") + } + is SendMessageResult.SlashCommandError -> { + displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) + } + is SendMessageResult.SlashCommandUnknown -> { + displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) + } + is SendMessageResult.SlashCommandResultOk -> { + updateComposerText("") + } + is SendMessageResult.SlashCommandResultError -> { + displayCommandError(sendMessageResult.throwable.localizedMessage) + } + is SendMessageResult.SlashCommandNotImplemented -> { + displayCommandError(getString(R.string.not_implemented)) } } - override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) { - ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData) - .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") - } + lockSendButton = false + } - override fun onEditedDecorationClicked(informationData: MessageInformationData) { - ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData) - .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") - } + private fun displayCommandError(message: String) { + AlertDialog.Builder(activity!!) + .setTitle(R.string.command_error) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } - override fun onRoomCreateLinkClicked(url: String) { - permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { - override fun navToRoom(roomId: String, eventId: String?): Boolean { - requireActivity().finish() - return false +// TimelineEventController.Callback ************************************************************ + + override fun onUrlClicked(url: String): Boolean { + return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String, eventId: String?): Boolean { + // Same room? + if (roomId == roomDetailArgs.roomId) { + // Navigation to same room + if (eventId == null) { + showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) + } else { + // Highlight and scroll to this event + roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, true)) + } + return true } - }) - } - override fun onReadReceiptsClicked(readReceipts: List) { - DisplayReadReceiptsBottomSheet.newInstance(readReceipts) - .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") - } + // Not handled + return false + } + }) + } - override fun onReadMarkerLongDisplayed() = withState(roomDetailViewModel) { state -> - val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() - val nextReadMarkerId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) - if (nextReadMarkerId != null) { - roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId)) + override fun onUrlLongClicked(url: String): Boolean { + if (url != getString(R.string.edited_suffix)) { + // Copy the url to the clipboard + copyToClipboard(requireContext(), url, true, R.string.link_copied_to_clipboard) + } + return true + } + + override fun onEventVisible(event: TimelineEvent) { + roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsVisible(event)) + } + + override fun onEventInvisible(event: TimelineEvent) { + roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsInvisible(event)) + } + + override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) { + vectorBaseActivity.notImplemented("encrypted message click") + } + + override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { + // TODO Use navigator + + val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) + val pairs = ArrayList>() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + requireActivity().window.decorView.findViewById(android.R.id.statusBarBackground)?.let { + pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) + } + requireActivity().window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { + pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) } } + pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) + pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) + pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) - // AutocompleteUserPresenter.Callback + val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), *pairs.toTypedArray()).toBundle() + startActivity(intent, bundle) + } - override fun onQueryUsers(query: CharSequence?) { - textComposerViewModel.process(TextComposerActions.QueryUsers(query)) + override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { + // TODO Use navigator + val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) + startActivity(intent) + } + + override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { + val action = RoomDetailActions.DownloadFile(eventId, messageFileContent) + // We need WRITE_EXTERNAL permission + if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { + roomDetailViewModel.process(action) + } else { + roomDetailViewModel.pendingAction = action } + } - private fun handleActions(action: SimpleAction) { - when (action) { - is SimpleAction.AddReaction -> { - startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (allGranted(grantResults)) { + if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) { + val action = roomDetailViewModel.pendingAction + + if (action != null) { + roomDetailViewModel.pendingAction = null + roomDetailViewModel.process(action) } - is SimpleAction.ViewReactions -> { - ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) - .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") - } - is SimpleAction.Copy -> { - //I need info about the current selected message :/ - copyToClipboard(requireContext(), action.content, false) - val msg = requireContext().getString(R.string.copied_to_clipboard) - showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) - } - is SimpleAction.Delete -> { - roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) - } - is SimpleAction.Share -> { - //TODO current data communication is too limited - //Need to now the media type - //TODO bad, just POC - BigImageViewer.imageLoader().loadImage( - action.hashCode(), - Uri.parse(action.imageUrl), - object : ImageLoader.Callback { - override fun onFinish() {} + } + } + } - override fun onSuccess(image: File?) { - if (image != null) - shareMedia(requireContext(), image, "image/*") - } + override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { + vectorBaseActivity.notImplemented("open audio file") + } - override fun onFail(error: Exception?) {} + override fun onLoadMore(direction: Timeline.Direction) { + roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) + } - override fun onCacheHit(imageType: Int, image: File?) {} + override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { - override fun onCacheMiss(imageType: Int, image: File?) {} + } - override fun onProgress(progress: Int) {} + override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean { + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + val roomId = roomDetailArgs.roomId - override fun onStart() {} + this.view?.hideKeyboard() + MessageActionsBottomSheet + .newInstance(roomId, informationData) + .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") + return true + } + override fun onAvatarClicked(informationData: MessageInformationData) { + vectorBaseActivity.notImplemented("Click on user avatar") + } + + @SuppressLint("SetTextI18n") + override fun onMemberNameClicked(informationData: MessageInformationData) { + insertUserDisplayNameInTextEditor(informationData.memberName?.toString()) + } + + override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { + if (on) { + //we should test the current real state of reaction on this event + roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId)) + } else { + //I need to redact a reaction + roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction)) + } + } + + override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) { + ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") + } + + override fun onEditedDecorationClicked(informationData: MessageInformationData) { + ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") + } + + override fun onRoomCreateLinkClicked(url: String) { + permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String, eventId: String?): Boolean { + requireActivity().finish() + return false + } + }) + } + + override fun onReadReceiptsClicked(readReceipts: List) { + DisplayReadReceiptsBottomSheet.newInstance(readReceipts) + .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") + } + + override fun onReadMarkerLongDisplayed() = withState(roomDetailViewModel) { state -> + val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() + val nextReadMarkerId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) + if (nextReadMarkerId != null) { + roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId)) + } + } + + // AutocompleteUserPresenter.Callback + + override fun onQueryUsers(query: CharSequence?) { + textComposerViewModel.process(TextComposerActions.QueryUsers(query)) + } + + private fun handleActions(action: SimpleAction) { + when (action) { + is SimpleAction.AddReaction -> { + startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) + } + is SimpleAction.ViewReactions -> { + ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) + .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") + } + is SimpleAction.Copy -> { + //I need info about the current selected message :/ + copyToClipboard(requireContext(), action.content, false) + val msg = requireContext().getString(R.string.copied_to_clipboard) + showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) + } + is SimpleAction.Delete -> { + roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) + } + is SimpleAction.Share -> { + //TODO current data communication is too limited + //Need to now the media type + //TODO bad, just POC + BigImageViewer.imageLoader().loadImage( + action.hashCode(), + Uri.parse(action.imageUrl), + object : ImageLoader.Callback { + override fun onFinish() {} + + override fun onSuccess(image: File?) { + if (image != null) + shareMedia(requireContext(), image, "image/*") } - ) - } - is SimpleAction.ViewEditHistory -> { - onEditedDecorationClicked(action.messageInformationData) - } - is SimpleAction.ViewSource -> { - val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) - view.findViewById(R.id.event_content_text_view)?.let { - it.text = action.content - } - AlertDialog.Builder(requireActivity()) - .setView(view) - .setPositiveButton(R.string.ok, null) - .show() - } - is SimpleAction.ViewDecryptedSource -> { - val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) - view.findViewById(R.id.event_content_text_view)?.let { - it.text = action.content - } + override fun onFail(error: Exception?) {} - AlertDialog.Builder(requireActivity()) - .setView(view) - .setPositiveButton(R.string.ok, null) - .show() - } - is SimpleAction.QuickReact -> { - //eventId,ClickedOn,Add - roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) - } - is SimpleAction.Edit -> { - roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString())) - } - is SimpleAction.Quote -> { - roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString())) - } - is SimpleAction.Reply -> { - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString())) - } - is SimpleAction.CopyPermalink -> { - val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) - copyToClipboard(requireContext(), permalink, false) - showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) + override fun onCacheHit(imageType: Int, image: File?) {} + override fun onCacheMiss(imageType: Int, image: File?) {} + + override fun onProgress(progress: Int) {} + + override fun onStart() {} + + } + ) + } + is SimpleAction.ViewEditHistory -> { + onEditedDecorationClicked(action.messageInformationData) + } + is SimpleAction.ViewSource -> { + val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) + view.findViewById(R.id.event_content_text_view)?.let { + it.text = action.content } - is SimpleAction.Resend -> { - roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId)) - } - is SimpleAction.Remove -> { - roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId)) - } - else -> { - Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() + + AlertDialog.Builder(requireActivity()) + .setView(view) + .setPositiveButton(R.string.ok, null) + .show() + } + is SimpleAction.ViewDecryptedSource -> { + val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) + view.findViewById(R.id.event_content_text_view)?.let { + it.text = action.content } + + AlertDialog.Builder(requireActivity()) + .setView(view) + .setPositiveButton(R.string.ok, null) + .show() + } + is SimpleAction.QuickReact -> { + //eventId,ClickedOn,Add + roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) + } + is SimpleAction.Edit -> { + roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString())) + } + is SimpleAction.Quote -> { + roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString())) + } + is SimpleAction.Reply -> { + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString())) + } + is SimpleAction.CopyPermalink -> { + val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) + copyToClipboard(requireContext(), permalink, false) + showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) + + } + is SimpleAction.Resend -> { + roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId)) + } + is SimpleAction.Remove -> { + roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId)) + } + else -> { + Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } } + } //utils - /** - * Insert an user displayname in the message editor. - * - * @param text the text to insert. - */ + /** + * Insert an user displayname in the message editor. + * + * @param text the text to insert. + */ //TODO legacy, refactor - private fun insertUserDisplayNameInTextEditor(text: String?) { - //TODO move logic outside of fragment - if (null != text) { + private fun insertUserDisplayNameInTextEditor(text: String?) { + //TODO move logic outside of fragment + if (null != text) { // var vibrate = false - val myDisplayName = session.getUser(session.myUserId)?.displayName - if (TextUtils.equals(myDisplayName, text)) { - // current user - if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { - composerLayout.composerEditText.append(Command.EMOTE.command + " ") - composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) + val myDisplayName = session.getUser(session.myUserId)?.displayName + if (TextUtils.equals(myDisplayName, text)) { + // current user + if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { + composerLayout.composerEditText.append(Command.EMOTE.command + " ") + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) // vibrate = true + } + } else { + // another user + if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { + // Ensure displayName will not be interpreted as a Slash command + if (text.startsWith("/")) { + composerLayout.composerEditText.append("\\") } + composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ") } else { - // another user - if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { - // Ensure displayName will not be interpreted as a Slash command - if (text.startsWith("/")) { - composerLayout.composerEditText.append("\\") - } - composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ") - } else { - composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ") - } + composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ") + } // vibrate = true - } + } // if (vibrate && vectorPreferences.vibrateWhenMentioning()) { // val v= context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator @@ -1122,44 +1122,44 @@ class RoomDetailFragment : // v.vibrate(100) // } // } - focusComposerAndShowKeyboard() - } + focusComposerAndShowKeyboard() } + } - private fun focusComposerAndShowKeyboard() { - composerLayout.composerEditText.requestFocus() - val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT) - } + private fun focusComposerAndShowKeyboard() { + composerLayout.composerEditText.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT) + } - private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { - val snack = Snackbar.make(view!!, message, duration) - snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color)) - snack.show() - } + private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { + val snack = Snackbar.make(view!!, message, duration) + snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color)) + snack.show() + } - // VectorInviteView.Callback + // VectorInviteView.Callback - override fun onAcceptInvite() { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) - roomDetailViewModel.process(RoomDetailActions.AcceptInvite) - } + override fun onAcceptInvite() { + notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) + roomDetailViewModel.process(RoomDetailActions.AcceptInvite) + } - override fun onRejectInvite() { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) - roomDetailViewModel.process(RoomDetailActions.RejectInvite) - } + override fun onRejectInvite() { + notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) + roomDetailViewModel.process(RoomDetailActions.RejectInvite) + } // JumpToReadMarkerView.Callback - override fun onJumpToReadMarkerClicked(readMarkerId: String) { - roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false)) - } - - override fun onClearReadMarkerClicked() { - roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead) - } - - + override fun onJumpToReadMarkerClicked(readMarkerId: String) { + roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false)) } + + override fun onClearReadMarkerClicked() { + roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead) + } + + +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index 62d80408d2..92e38c112b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -20,9 +20,15 @@ import androidx.recyclerview.widget.LinearLayoutManager import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.riotx.core.platform.DefaultListUpdateCallback import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.atomic.AtomicReference +/** + * This handles scrolling to an event which wasn't yet loaded when scheduled. + */ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { @@ -30,7 +36,15 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa var timeline: Timeline? = null + override fun onInserted(position: Int, count: Int) { + scrollIfNeeded() + } + override fun onChanged(position: Int, count: Int, tag: Any?) { + scrollIfNeeded() + } + + private fun scrollIfNeeded() { val eventId = scheduledEventId.get() ?: return val nonNullTimeline = timeline ?: return val correctedEventId = nonNullTimeline.getFirstDisplayableEventId(eventId)