From 45ea5c356e193a55969b50e1cde901298d16c43b Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 23 May 2019 16:44:51 +0200 Subject: [PATCH 01/17] WIP / edit message --- .../room/model/annotation/ReactionService.kt | 10 + .../api/session/room/send/SendService.kt | 1 + .../room/annotation/DefaultReactionService.kt | 20 ++ .../session/room/send/DefaultSendService.kt | 11 + .../room/send/LocalEchoEventFactory.kt | 25 ++ .../riotredesign/core/utils/AnimationUtils.kt | 39 +++ .../riotredesign/features/DebugActivity.kt | 12 + .../home/room/detail/RoomDetailActions.kt | 7 +- .../home/room/detail/RoomDetailFragment.kt | 96 +++++++- .../home/room/detail/RoomDetailViewModel.kt | 231 +++++++++++++----- .../home/room/detail/RoomDetailViewState.kt | 19 +- .../timeline/action/MessageMenuViewModel.kt | 22 +- .../drawable-hdpi/ic_attach_file_white.png | Bin 394 -> 0 bytes .../main/res/drawable-hdpi/ic_send_white.png | Bin 335 -> 0 bytes .../drawable-mdpi/ic_attach_file_white.png | Bin 285 -> 0 bytes .../main/res/drawable-mdpi/ic_send_white.png | Bin 257 -> 0 bytes .../drawable-xhdpi/ic_attach_file_white.png | Bin 507 -> 0 bytes .../main/res/drawable-xhdpi/ic_send_white.png | Bin 423 -> 0 bytes .../drawable-xxhdpi/ic_attach_file_white.png | Bin 809 -> 0 bytes .../res/drawable-xxhdpi/ic_send_white.png | Bin 550 -> 0 bytes .../drawable-xxxhdpi/ic_attach_file_white.png | Bin 1059 -> 0 bytes .../res/drawable-xxxhdpi/ic_send_white.png | Bin 728 -> 0 bytes .../src/main/res/drawable/ic_add_reaction.xml | 54 ++++ .../src/main/res/drawable/ic_attachment.xml | 14 ++ .../src/main/res/drawable/ic_close_round.xml | 20 ++ .../drawable/{ic_smile.xml => ic_delete.xml} | 20 +- vector/src/main/res/drawable/ic_edit.xml | 8 +- vector/src/main/res/drawable/ic_send.xml | 14 ++ .../main/res/layout/adapter_item_action.xml | 8 +- ...constraint_set_composer_layout_compact.xml | 153 ++++++++++++ ...onstraint_set_composer_layout_expanded.xml | 156 ++++++++++++ .../main/res/layout/fragment_room_detail.xml | 55 +---- .../res/layout/include_composer_layout.xml | 123 ++++++++++ vector/src/main/res/values/attrs.xml | 1 + vector/src/main/res/values/colors_riot.xml | 1 + vector/src/main/res/values/theme_black.xml | 1 + vector/src/main/res/values/theme_dark.xml | 1 + vector/src/main/res/values/theme_light.xml | 1 + vector/src/main/res/values/theme_status.xml | 1 + 39 files changed, 978 insertions(+), 146 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/DebugActivity.kt delete mode 100644 vector/src/main/res/drawable-hdpi/ic_attach_file_white.png delete mode 100644 vector/src/main/res/drawable-hdpi/ic_send_white.png delete mode 100644 vector/src/main/res/drawable-mdpi/ic_attach_file_white.png delete mode 100644 vector/src/main/res/drawable-mdpi/ic_send_white.png delete mode 100644 vector/src/main/res/drawable-xhdpi/ic_attach_file_white.png delete mode 100644 vector/src/main/res/drawable-xhdpi/ic_send_white.png delete mode 100644 vector/src/main/res/drawable-xxhdpi/ic_attach_file_white.png delete mode 100644 vector/src/main/res/drawable-xxhdpi/ic_send_white.png delete mode 100644 vector/src/main/res/drawable-xxxhdpi/ic_attach_file_white.png delete mode 100644 vector/src/main/res/drawable-xxxhdpi/ic_send_white.png create mode 100644 vector/src/main/res/drawable/ic_add_reaction.xml create mode 100644 vector/src/main/res/drawable/ic_attachment.xml create mode 100644 vector/src/main/res/drawable/ic_close_round.xml rename vector/src/main/res/drawable/{ic_smile.xml => ic_delete.xml} (51%) create mode 100644 vector/src/main/res/drawable/ic_send.xml create mode 100644 vector/src/main/res/layout/constraint_set_composer_layout_compact.xml create mode 100644 vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml create mode 100644 vector/src/main/res/layout/include_composer_layout.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt index ace61159..3f8fec00 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.model.annotation import im.vector.matrix.android.api.util.Cancelable +//TODO rename in relationService? interface ReactionService { @@ -49,4 +50,13 @@ interface ReactionService { */ fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) + + /** + * Edit a text message body. Limited to "m.text" contentType + * @param targetEventId The event to edit + * @param newBodyText The edited body + * @param compatibilityBodyText The text that will appear on clients that don't support yet edition + */ + fun editTextMessage(targetEventId: String, newBodyText: String, compatibilityBodyText: String = "* $newBodyText"): Cancelable + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 6852931c..551ab545 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -34,6 +34,7 @@ interface SendService { * @return a [Cancelable] */ fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable + fun sendFormattedTextMessage(text: String,formattedText: String): Cancelable /** * Method to send a media asynchronously. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt index dbef5461..fdb75a8f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt @@ -19,6 +19,7 @@ import androidx.work.* import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.annotation.ReactionService +import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.RedactEventWorker @@ -150,4 +151,23 @@ internal class DefaultReactionService(private val roomId: String, .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } + + override fun editTextMessage(targetEventId: String, newBodyText: String, compatibilityBodyText: String): Cancelable { + val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, MessageType.MSGTYPE_TEXT, compatibilityBodyText) + val sendContentWorkerParams = SendEventWorker.Params(roomId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + //TODO use relation API? + val workRequest = OneTimeWorkRequestBuilder() + .setConstraints(WORK_CONSTRAINTS) + .setInputData(sendWorkData) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest) + .enqueue() + return CancelableWork(workRequest.id) + + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 28ae63b8..1d6197a3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -60,6 +60,17 @@ internal class DefaultSendService(private val roomId: String, return CancelableWork(sendWork.id) } + override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable { + val event = eventFactory.createFormattedTextEvent(roomId, text, formattedText).also { + saveLocalEcho(it) + } + val sendWork = createSendEventWork(event) + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork) + .enqueue() + return CancelableWork(sendWork.id) + } + override fun sendMedias(attachments: List): Cancelable { val cancelableBag = CancelableBag() attachments.forEach { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index a2076d3b..e1cc9c74 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent import im.vector.matrix.android.api.session.room.model.annotation.ReactionInfo +import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.session.content.ThumbnailExtractor @@ -35,6 +36,30 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { return createEvent(roomId, content) } + fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event { + val content = MessageTextContent( + type = MessageType.MSGTYPE_TEXT, + format = MessageType.FORMAT_MATRIX_HTML, + body = text, + formattedBody = formattedText + ) + return createEvent(roomId, content) + } + + + fun createReplaceTextEvent(roomId: String, targetEventId: String, newBodyText: String, msgType: String, compatibilityText: String): Event { + val content = MessageTextContent( + type = msgType, + body = compatibilityText, + relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), + newContent = MessageTextContent( + type = MessageType.MSGTYPE_TEXT, + body = newBodyText + ).toContent() + ) + return createEvent(roomId, content) + } + fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event { return when (attachment.type) { ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt new file mode 100644 index 00000000..a773991d --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt @@ -0,0 +1,39 @@ +package im.vector.riotredesign.core.utils + +import android.view.animation.OvershootInterpolator +import androidx.annotation.LayoutRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.transition.ChangeBounds +import androidx.transition.Transition +import androidx.transition.TransitionManager + + +inline fun ConstraintLayout.updateConstraintSet(@LayoutRes layoutId: Int, rootLayoutForAnimation: ConstraintLayout? = null, noinline onAnimationEnd: (() -> Unit)? = null) { + if (rootLayoutForAnimation != null) { + val transition = ChangeBounds() + transition.interpolator = OvershootInterpolator() + transition.addListener(object : Transition.TransitionListener { + override fun onTransitionResume(transition: Transition) { + } + + override fun onTransitionPause(transition: Transition) { + } + + override fun onTransitionCancel(transition: Transition) { + } + + override fun onTransitionStart(transition: Transition) { + } + + override fun onTransitionEnd(transition: Transition) { + onAnimationEnd?.invoke() + } + }) + TransitionManager.beginDelayedTransition(rootLayoutForAnimation, transition) + } + ConstraintSet().also { + it.clone(this@updateConstraintSet.context, layoutId) + it.applyTo(this@updateConstraintSet) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/DebugActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/DebugActivity.kt new file mode 100644 index 00000000..00708d03 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/DebugActivity.kt @@ -0,0 +1,12 @@ +package im.vector.riotredesign.features + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle + +class DebugActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_debug) + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 7d6ffc04..190c37de 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -31,10 +31,13 @@ sealed class RoomDetailActions { data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() - data class UpdateQuickReactAction(val targetEventId: String,val selectedReaction: String,val opposite: String) : RoomDetailActions() - data class ShowEditHistoryAction(val event: String,val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions() + data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val opposite: String) : RoomDetailActions() + data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions() object AcceptInvite : RoomDetailActions() object RejectInvite : RoomDetailActions() + data class EnterEditMode(val eventId: String) : RoomDetailActions() + data class EnterQuoteMode(val eventId: String) : RoomDetailActions() + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 0278607a..0ae0dda5 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -32,14 +32,17 @@ import android.view.HapticFeedbackConstants import android.view.LayoutInflater import android.view.View import android.view.inputmethod.InputMethodManager +import android.widget.ImageButton import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.fragmentViewModel @@ -53,6 +56,7 @@ import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.* @@ -92,6 +96,7 @@ import im.vector.riotredesign.features.media.VideoMediaViewerActivity import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* +import kotlinx.android.synthetic.main.include_composer_layout.* import org.koin.android.ext.android.inject import org.koin.android.scope.ext.android.bindScope import org.koin.android.scope.ext.android.getOrCreateScope @@ -165,6 +170,15 @@ class RoomDetailFragment : private lateinit var actionViewModel: ActionsHandler + @BindView(R.id.composer_related_message_sender) + lateinit var composerRelatedMessageTitle: TextView + @BindView(R.id.composer_related_message_preview) + lateinit var composerRelatedMessageContent: TextView + @BindView(R.id.composerLayout) + lateinit var composerLayout: ConstraintLayout + @BindView(R.id.rootConstraintLayout) + lateinit var rootConstraintLayout: ConstraintLayout + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) @@ -187,6 +201,65 @@ class RoomDetailFragment : actionViewModel.actionCommandEvent.observe(this, Observer { handleActions(it) }) + + roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::selectedEvent, RoomDetailViewState::roomId) { mode, event, roomId -> + when (mode) { + SendMode.REGULAR -> { + val uid = session.sessionParams.credentials.userId + val meMember = session.getRoom(roomId)?.getRoomMember(uid) + AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composer_avatar_view) + composerLayout.updateConstraintSet(R.layout.constraint_set_composer_layout_compact, rootConstraintLayout) { + focusComposerAndShowKeyboard() + } + } + SendMode.EDIT, + SendMode.QUOTE -> { + if (event == null) { + //we should ignore? can this happen? + Timber.e("Enter edit mode with no event selected") + return@selectSubscribe + } + //switch to expanded bar + composerRelatedMessageTitle.text = event.senderName + composerRelatedMessageTitle.setTextColor( + ContextCompat.getColor(requireContext(), AvatarRenderer.getColorFromUserId(event.root.sender + ?: "")) + ) + + val messageContent: MessageContent? = + event.annotations?.editSummary?.aggregatedContent?.toModel() + ?: event.root.content.toModel() + val eventTextBody = messageContent?.body + composerRelatedMessageContent.text = eventTextBody + + + if (mode == SendMode.EDIT) { + composerEditText.setText(eventTextBody) + composer_related_message_action_image.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit)) + } else { + composerEditText.setText("") + composer_related_message_action_image.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_quote)) + } + + AvatarRenderer.render(event.senderAvatar, event.root.sender + ?: "", event.senderName, composer_avatar_view) + + composerEditText.setSelection(composerEditText.text.length) + composerLayout.updateConstraintSet(R.layout.constraint_set_composer_layout_expanded, rootConstraintLayout) { + focusComposerAndShowKeyboard() + } + + view?.findViewById(R.id.composer_related_message_close)?.setOnClickListener { + + composerRelatedMessageTitle.text = "" + composerRelatedMessageContent.text = "" + composerEditText.setText("") + roomDetailViewModel.resetSendMode() + } + + } + } + } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -398,6 +471,11 @@ class RoomDetailFragment : if (summary?.membership == Membership.JOIN) { timelineEventController.setTimeline(state.timeline) inviteView.visibility = View.GONE + + val uid = session.sessionParams.credentials.userId + val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) + AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composer_avatar_view) + } else if (summary?.membership == Membership.INVITE && inviter != null) { inviteView.visibility = View.VISIBLE inviteView.render(inviter, VectorInviteView.Mode.LARGE) @@ -601,6 +679,14 @@ class RoomDetailFragment : roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite)) } } + MessageMenuViewModel.ACTION_EDIT -> { + val eventId = actionData.data.toString() ?: return@let + roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId)) + } + MessageMenuViewModel.ACTION_QUOTE -> { + val eventId = actionData.data.toString() ?: return@let + roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(eventId)) + } else -> { Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() } @@ -648,12 +734,16 @@ class RoomDetailFragment : // v.vibrate(100) // } // } - composerEditText.requestFocus() - val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.showSoftInput(composerEditText, InputMethodManager.SHOW_FORCED) + focusComposerAndShowKeyboard() } } + private fun focusComposerAndShowKeyboard() { + composerEditText.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showSoftInput(composerEditText, InputMethodManager.SHOW_IMPLICIT) + } + fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { val snack = Snackbar.make(view!!, message, Snackbar.LENGTH_SHORT) snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color)) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 44c2aa8f..f5664efc 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotredesign.features.home.room.detail +import android.text.TextUtils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.MvRxViewModelFactory @@ -25,8 +26,11 @@ import com.jakewharton.rxrelay2.BehaviorRelay import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.rx.rx import im.vector.riotredesign.R import im.vector.riotredesign.core.platform.VectorViewModel @@ -36,11 +40,14 @@ import im.vector.riotredesign.features.command.ParsedCommand import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import io.reactivex.rxkotlin.subscribeBy +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer import org.koin.android.ext.android.get import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.TimeUnit + class RoomDetailViewModel(initialState: RoomDetailViewState, private val session: Session, private val visibleRoomHolder: VisibleRoomStore @@ -87,9 +94,28 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, is RoomDetailActions.UndoReaction -> handleUndoReact(action) is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action) is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action) + is RoomDetailActions.EnterEditMode -> handleEditAction(action) + is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) } } + fun enterEditMode(event: TimelineEvent) { + setState { + copy( + sendMode = SendMode.EDIT, + selectedEvent = event + ) + } + } + + fun resetSendMode() { + setState { + copy( + sendMode = SendMode.REGULAR, + selectedEvent = null + ) + } + } private val _nonBlockingPopAlert = MutableLiveData>>>() val nonBlockingPopAlert: LiveData>>> @@ -103,71 +129,135 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, // PRIVATE METHODS ***************************************************************************** private fun handleSendMessage(action: RoomDetailActions.SendMessage) { - // Handle slash command - val slashCommandResult = CommandParser.parseSplashCommand(action.text) + withState { state -> + when (state.sendMode) { + SendMode.REGULAR -> { + val slashCommandResult = CommandParser.parseSplashCommand(action.text) - when (slashCommandResult) { - is ParsedCommand.ErrorNotACommand -> { - // Send the text message to the room - room.sendTextMessage(action.text) - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) - } - is ParsedCommand.ErrorSyntax -> { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))) - } - is ParsedCommand.ErrorEmptySlashCommand -> { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/"))) - } - is ParsedCommand.ErrorUnknownSlashCommand -> { - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) - } - is ParsedCommand.Invite -> { - handleInviteSlashCommand(slashCommandResult) - } - is ParsedCommand.SetUserPowerLevel -> { - // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) - } - is ParsedCommand.ClearScalarToken -> { - // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) - } - is ParsedCommand.SetMarkdown -> { - // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) - } - is ParsedCommand.UnbanUser -> { - // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) - } - is ParsedCommand.BanUser -> { - // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) - } - is ParsedCommand.KickUser -> { - // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) - } - is ParsedCommand.JoinRoom -> { - // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) - } - is ParsedCommand.PartRoom -> { - // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) - } - is ParsedCommand.SendEmote -> { - room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) - } - is ParsedCommand.ChangeTopic -> { - handleChangeTopicSlashCommand(slashCommandResult) - } - is ParsedCommand.ChangeDisplayName -> { - // TODO - _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + when (slashCommandResult) { + is ParsedCommand.ErrorNotACommand -> { + // Send the text message to the room + room.sendTextMessage(action.text) + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + } + is ParsedCommand.ErrorSyntax -> { + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))) + } + is ParsedCommand.ErrorEmptySlashCommand -> { + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/"))) + } + is ParsedCommand.ErrorUnknownSlashCommand -> { + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) + } + is ParsedCommand.Invite -> { + handleInviteSlashCommand(slashCommandResult) + } + is ParsedCommand.SetUserPowerLevel -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.ClearScalarToken -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.SetMarkdown -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.UnbanUser -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.BanUser -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.KickUser -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.JoinRoom -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.PartRoom -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + is ParsedCommand.SendEmote -> { + room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) + } + is ParsedCommand.ChangeTopic -> { + handleChangeTopicSlashCommand(slashCommandResult) + } + is ParsedCommand.ChangeDisplayName -> { + // TODO + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) + } + } + } + SendMode.EDIT -> { + room.editTextMessage(state?.selectedEvent?.root?.eventId ?: "", action.text) + setState { + copy( + sendMode = SendMode.REGULAR, + selectedEvent = null + ) + } + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + } + SendMode.QUOTE -> { + withState { state -> + val messageContent: MessageContent? = + state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel() + ?: state.selectedEvent?.root?.content.toModel() + val textMsg = messageContent?.body + + val finalText = legacyRiotQuoteText(textMsg, action.text) + + //TODO Refactor this, just temporary for quotes + val parser = Parser.builder().build() + val document = parser.parse(finalText) + val renderer = HtmlRenderer.builder().build() + val htmlText = renderer.render(document) + if (TextUtils.equals(finalText, htmlText)) { + room.sendTextMessage(finalText) + } else { + room.sendFormattedTextMessage(finalText, htmlText) + } + setState { + copy( + sendMode = SendMode.REGULAR, + selectedEvent = null + ) + } + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + } + + } } } + // Handle slash command + + } + + private fun legacyRiotQuoteText(quotedText: String?, myText: String): String { + val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() + var quotedTextMsg = StringBuilder() + if (messageParagraphs != null) { + for (i in messageParagraphs.indices) { + if (messageParagraphs[i].trim({ it <= ' ' }) != "") { + quotedTextMsg.append("> ").append(messageParagraphs[i]) + } + + if (i + 1 != messageParagraphs.size) { + quotedTextMsg.append("\n\n") + } + } + } + val finalText = "$quotedTextMsg\n\n$myText" + return finalText } private fun handleShowEditHistoryReaction(action: RoomDetailActions.ShowEditHistoryAction) { @@ -271,6 +361,23 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, room.join(object : MatrixCallback {}) } + private fun handleEditAction(action: RoomDetailActions.EnterEditMode) { + room.getTimeLineEvent(action.eventId)?.let { + enterEditMode(it) + } + } + + private fun handleQuoteAction(action: RoomDetailActions.EnterQuoteMode) { + room.getTimeLineEvent(action.eventId)?.let { + setState { + copy( + sendMode = SendMode.QUOTE, + selectedEvent = it + ) + } + } + } + private fun observeEventDisplayedActions() { // We are buffering scroll events for one second diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt index 43fbe9bd..00ce0b47 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt @@ -22,15 +22,32 @@ import com.airbnb.mvrx.Uninitialized import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineData +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.user.model.User +/** + * Describes the current send mode: + * REGULAR: sends the text as a regular message + * QUOTE: User is currently quoting a message + * EDIT: User is currently editing an existing message + * + * Depending on the state the bottom toolbar will change (icons/preview/actions...) + */ +enum class SendMode { + REGULAR, + QUOTE, + EDIT +} + data class RoomDetailViewState( val roomId: String, val eventId: String?, val timeline: Timeline? = null, val inviter: Async = Uninitialized, val asyncRoomSummary: Async = Uninitialized, - val asyncTimelineData: Async = Uninitialized + val asyncTimelineData: Async = Uninitialized, + val sendMode: SendMode = SendMode.REGULAR, + val selectedEvent: TimelineEvent? = null ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index c39da484..4fee2f41 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -59,7 +59,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel().apply { - this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_smile, event.root.eventId)) + this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, event.root.eventId)) if (canCopy(type)) { //TODO copy images? html? see ClipBoard this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent.body)) } + if (canEdit(event, currentSession.sessionParams.credentials.userId)) { + this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, event.root.eventId)) + } + if (canRedact(event, currentSession.sessionParams.credentials.userId)) { - this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_material_delete, event.root.eventId)) + this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId)) } if (canQuote(event, messageContent)) { @@ -159,6 +163,17 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel() + return event.root.sender == myUserId && ( + messageContent?.type == MessageType.MSGTYPE_TEXT + || messageContent?.type == MessageType.MSGTYPE_EMOTE + ) + } + private fun canCopy(type: String): Boolean { return when (type) { @@ -187,6 +202,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel5IQ?#}RC|!%)lu~tD_qrI-aTEW6e;_S7 zn$cviB=CU~-+TGUL@+NUgh(mkfI1ie5BT5%G(b8=yXu!qXh!~eKLlZ(C7t2M&Trg&7LMR-kiu?aUyB0n) z&I(x}D`bVNkQK5*R_NS?7JR;x<_etik`Q{~b01t$=9P0#NeGp}e9L8V21-c?g;LLa zMILvibj*Kj6`?f?tK3R6%<**yO<4R!A*aTiZyiEC7H=x#w3yS@Ayj4Y6{s8#f6Z4V z_*qLk=8+5h?2!rtri|_A><6URZ-!^4CGE_cYHKFWbvG^TRMhCi=@T8( oehbc27mTE>CY>q5uE@07*qoM6N<$f}e__;Q#;t diff --git a/vector/src/main/res/drawable-hdpi/ic_send_white.png b/vector/src/main/res/drawable-hdpi/ic_send_white.png deleted file mode 100644 index f133cdbeb2757f5d4f51321c302c7fcd81be52fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 335 zcmV-V0kHmwP)jA}WRMU8oC{kc85)g#pw;5z59ljG-BdP&R(yv#6YTg#Hfm zoUTIVIi*5oIemrHb7~5yx+gicg`VIQ-e3wV*yj+6B|>Sk20TL_MlgpBdJNAf zVcN%%X|KI7{+s0%pN@-Jiu}0d%3{6!aL!`2K@(@u-okok(b(jxX7M=&nPib16iu_- h^-;R~`%wzHas(7`hA~Sk`s4rr002ovPDHLkV1mtQjA#G= diff --git a/vector/src/main/res/drawable-mdpi/ic_attach_file_white.png b/vector/src/main/res/drawable-mdpi/ic_attach_file_white.png deleted file mode 100644 index a819f4034d49512d474d5dd27781f40d6837bd0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 285 zcmV+&0pk9NP)5&yolhb5=1KtwTyP^0hF9Tv=VP27K)%~8d2A8;S&~#`Pr;p0*~y@ z4)caBb}S);^-t0yH9!DDa03faHzkn8FW>U}-||0v^{6Sb;l>R~h%1 zrzO~fNkYyH>~sZVKFg&a=QgLDuPbmk6E+bNS$xX|DtTsdi(Se>Q_$wMd}7VKLtjz@ jBi{9ce?PACtO9M`=wB&0PW1Bf00000NkvXXu0mjfmu++^ diff --git a/vector/src/main/res/drawable-mdpi/ic_send_white.png b/vector/src/main/res/drawable-mdpi/ic_send_white.png deleted file mode 100644 index 34e49af7ab119a8581624cba15d1874ddb3ee8d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 257 zcmV+c0sj7pP)IG(5t990^tH2frgqR&{1*(Y8sBf1tc7SR!1$0@ii-(STgUK*D%SiQ2mo{ z?QG3}$&cECGd@<$#Ov@pO7 z8ys=h=JS168I;gK4->4g$K_K#$v_!R^fAR62V7GI7O15qs-le{=GfvSNHjvh;vsjC z4gEKLW(fVFy<`Y=RlLpc&1L76L33S;GDto+sto`0O*ZiWxujSd^@pKl00000NkvXX Hu0mjfBSB_$ diff --git a/vector/src/main/res/drawable-xhdpi/ic_attach_file_white.png b/vector/src/main/res/drawable-xhdpi/ic_attach_file_white.png deleted file mode 100644 index c572c35997127449a4b660c4c9f1cd5c70ff7a1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 507 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(Fdp)BaSW-r^>)^Iufq-k$1ZmV z=4-tQtP-@HdT;e3W|5DYQCpWcl?yt4(0hB(UEsfnyhCl%BKD2j7uh-L=GsW4Cw3hc znv`qi>o%wH{iJi}>h^EAIc2WJzW3Jkk$nt*R&iuCSt;;;<;rT{n!}jpAYNBdqkPB2 zoFVn!Mf3jOOB8dsUzGn73UQk3CA~mfclklq1uG<1w?C1~I=%7$6sN*7oD?OdD&O^L7z{|))m3ZrBRto7dme*i0P5aIwQPtGf)-Ni^)E5 zoq>-VdfqoayOd`*SAh8y>o(J4H*#&#ZAo!J6cZ?V89ZJ6T-G@yGywo`ncEKl diff --git a/vector/src/main/res/drawable-xhdpi/ic_send_white.png b/vector/src/main/res/drawable-xhdpi/ic_send_white.png deleted file mode 100644 index e5f9ba41acdb7d7f9353981e1aa0bcff06353b14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 423 zcmV;Y0a*TtP)prx+ z$_|d)F5m&)eB>qlT-LgJFVHE= z%W~f$arHimd07iA!6xj(DO|!MyjKu7gH_FhR$v3wd;=dyV1##h Rc_07)002ovPDHLkV1lk5y~_Xq diff --git a/vector/src/main/res/drawable-xxhdpi/ic_attach_file_white.png b/vector/src/main/res/drawable-xxhdpi/ic_attach_file_white.png deleted file mode 100644 index a5dc29f0d272d854bf2ad39ead95d01b1d791d47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 809 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2VEW+c;uunK>+PJo-lC2Y?X8RF zHvMAOIw7+9wbJ5xC9N0B_$R*5TQplwh4YfqZY>Qa)jj9!Jhb}6*hJ&R1H;2qYBj=2 zij6#0Zr;oM{$9`fH|H#?)9%jwyZ1vupVjAcXNuF(%Tm8@VR5{`bjE>+<*9#w`bB>q z!OusGjF0cP_Ri5=w1DrC-beio{?KP;7vn@6ZI8TP@GnB8@0Z>ReGj?x8qq7lhA-Y9 zkuW^6;=8@lDfy4S0qqYrL@88thAs4yE}Zj2?aFW0oR6LtWD_Ukcl3&tnhWN){@Pw6 zct!I~827XyC$rCqE8ge5wc~gtZuj>d+ow4KpT7K5$nc7o5G{S;vYO5v{~7_4^}JbY zI}b07 z%mT&htZQS|YBbelrOHVZ*$U-7*KS#G{LmST1ye0m&TQ#i#9^g2CD=$#XnMu7D=kLt z-AW#Jzy7jXp&uu-bP-2T4u9A^ojk6u6;9!|$`|ewO4;qKxlM4rPMTl1OP^!+T)p5X zv8{aZoto?Uq`$`8P&vP1{~xP^*?yYK{)MjHn{|z~_KH=_`9N#M$5GYtoAfe*UVlhG z-t=tCH?2tVlDG)>GOMiY?H8`?T*G}!yQJz!VDw_A=(~~PODfiv{_;QLcZI{cY3ljb zGbWpPr#Ja=&wo?gEqhXIYI;=0pkVOl1Jf%f=A`H2Ys|8 zpRBlX{f*(0j}9k%4=2QZh%DKgeQt~5{SDp!mMePr{!DLk)ZKU>RxJAGiVX_wAE!i2 z4tMP06`XYFhu59wr`x*!Sn17S@hZBu`&IR<^9!ac^KExazH#+TB(J05?@oWtqi1$S zOxUdCv2mJ(>z>uYXL#m6pK|$Cw%EQS8T|3qp+B_tocjN6#i#S4|Ej-dec_Kk%<3rc bWXgYOqoS1K?z8p+vjBsqtDnm{r-UW|NG4=C diff --git a/vector/src/main/res/drawable-xxhdpi/ic_send_white.png b/vector/src/main/res/drawable-xxhdpi/ic_send_white.png deleted file mode 100644 index 0ba718b6e25df33c10d4a2b91af75d08ccf25eff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 550 zcmV+>0@?kEP)2^SE1z*7@_A8XwdQqbbcxxfm#$i0yQJUBhX4kjY*Dg#$+aQ-5}@Qea_zJ ze0ye9>DKIZ|I={JHOO!%fr2QAf+&cBD2Re6=qGnz3rtW@IbHWX9dHC@D5zXee?Gtk zSf-$IL4(uIB|l$5ZXmzIl=G`9sF>eUPJ(*QZ<5MSLQqoqNeW6LzYu~<=NDR#x%__n zF~q3PFSa1{`DqeV%x{{4{?4xzJ3q~W2In4Z#>mf#plJD76BIl@tAc{%mx3VC{8AEB z%&#deXcEkWHLwd#!8LdSpSk4cYjzCGf)%h0j=?2(0Iw% zKaF?Lp||e^91P3P+7mv>?RziF&!V6?unKm-3Ah4};H?Hh=U~x7Vhdm$?13|I1D*vF z+Xqvc4$@LvFzP{i1B}Ny&4=8AN*PIAC~DY(TFz8eZZ$M+flo#vw;EPw4AVNVDL_ripK@?=1FD5h?{9Ic8+W-In07*qoM6N<$f?2ikg8%>k diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_attach_file_white.png b/vector/src/main/res/drawable-xxxhdpi/ic_attach_file_white.png deleted file mode 100644 index 60147bc7a378eb0cb32cfe4cc9ae61b4bcd046de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1059 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V1DK4;uunK>+RgT-Xeh#?Yae~ zckT3?4kc+`+x7aj(g*ny9~P}S>$7sV6YF~JUB!X<2+ua>mL+ndNLZnCCJu z3NR=vP&wTxmo%fot48(N%*=njf7j3MZ)I@KQF80BJYN0FGe$j1VL{C_LFXg#%l|A{ z*T?N*%km>x`Qs(S>1rJ&EEUPtACDT&w-b(5Jn;7n|Dw;`s!J3XnDENA&Aop_W1?b^ zi|*t{_wzm&<{b{SWzk7FvGnmKm(A@(*MySwQs=fevm_h5q6*jOVn^_FWSA zFkkTO&0Fz~JDFz{wJR!|=Q^vToxPwY(_)tDB!LO{<0j#?qi;39@sYWc?Ks_OFiATFP^6W87wg)m4n2ot|HjTC>=1g)%#1 zQPi5R4h$?H<^bV$@-+yYGcXqLH-5%!yk8N*;KdMyjM6- zzu!Z@9hK>}y)9+18_h4O~B_ z3w#jde*04NmXY`|PQk6-|9X?Z@$QOC3(n8w>)gh&CHvX?<4!rpIeo2fJ<>_w{ODBt zGM{fD?`@f%%ns)dz2cs&uX}gBjlay{x6&QUbxgp`2V%z*}akdsd3vx z%crnt_*p(&?l{}f-M8VLuPF>e5TLF$o2K^ad*bDe-$@2&poQP;DB8m{|v9J z*4^q}hHkMAjj=~^&+4txTe-G3I`Qq>BcTqBZ0}BNKEhm=ViEhrC*tqluK6h|+cm9q oZIe6JHr#0RYhd7D5c|pAuy^9+A9F0;05d;>r>mdKI;Vst01_g}=l}o! diff --git a/vector/src/main/res/drawable-xxxhdpi/ic_send_white.png b/vector/src/main/res/drawable-xxxhdpi/ic_send_white.png deleted file mode 100644 index f02b6453d5c3a05f683d2c49aaa8495033a14b8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 728 zcmV;}0w?{6P)4YaZvw*Q$*X|jJas(hsmet^laJvtPmu(;%u_@GuJROFfQvj;xhT@; z<>_foc`8l7`8?f1K;d~p!0&mQK|n!yLV$6es$6tMTo+5?vDg+b#K)GOmKBkwx_@9& zTorf3L$N6iQ~!;*??Lml*JPgBZd~Nm^G2-H&6DMK;3%u7R-SU+sPeXY26>u~BEa+N zshp>%54&?&J)gzezw%V#%TN}p=jd;FDpkNWu_zviE%97@5IR2x&m@Is-WXJGhPLR$&5Dvz2j;^ zod@ATK + + + + + + + diff --git a/vector/src/main/res/drawable/ic_attachment.xml b/vector/src/main/res/drawable/ic_attachment.xml new file mode 100644 index 00000000..e54b9302 --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/drawable/ic_close_round.xml b/vector/src/main/res/drawable/ic_close_round.xml new file mode 100644 index 00000000..413a233b --- /dev/null +++ b/vector/src/main/res/drawable/ic_close_round.xml @@ -0,0 +1,20 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_smile.xml b/vector/src/main/res/drawable/ic_delete.xml similarity index 51% rename from vector/src/main/res/drawable/ic_smile.xml rename to vector/src/main/res/drawable/ic_delete.xml index e2f3402e..b740db3c 100644 --- a/vector/src/main/res/drawable/ic_smile.xml +++ b/vector/src/main/res/drawable/ic_delete.xml @@ -4,31 +4,19 @@ android:viewportWidth="22" android:viewportHeight="22"> - - diff --git a/vector/src/main/res/drawable/ic_edit.xml b/vector/src/main/res/drawable/ic_edit.xml index ec5cf418..1ad914fc 100644 --- a/vector/src/main/res/drawable/ic_edit.xml +++ b/vector/src/main/res/drawable/ic_edit.xml @@ -4,19 +4,19 @@ android:viewportWidth="21" android:viewportHeight="22"> diff --git a/vector/src/main/res/drawable/ic_send.xml b/vector/src/main/res/drawable/ic_send.xml new file mode 100644 index 00000000..d79ba7c1 --- /dev/null +++ b/vector/src/main/res/drawable/ic_send.xml @@ -0,0 +1,14 @@ + + + diff --git a/vector/src/main/res/layout/adapter_item_action.xml b/vector/src/main/res/layout/adapter_item_action.xml index 5ee60d32..ce518071 100644 --- a/vector/src/main/res/layout/adapter_item_action.xml +++ b/vector/src/main/res/layout/adapter_item_action.xml @@ -3,7 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:layout_height="50dp" android:clickable="true" android:focusable="true" android:foreground="?attr/selectableItemBackground" @@ -12,7 +11,8 @@ android:paddingLeft="@dimen/layout_horizontal_margin" android:paddingTop="8dp" android:paddingRight="@dimen/layout_horizontal_margin" - android:paddingBottom="8dp"> + android:paddingBottom="8dp" + tools:layout_height="50dp"> + tools:src="@drawable/ic_delete" + android:tint="?android:attr/textColorTertiary" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml new file mode 100644 index 00000000..9a354fc2 --- /dev/null +++ b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index a5711bce..0f631722 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -2,6 +2,7 @@ @@ -89,66 +90,18 @@ android:background="?vctr_list_divider_color" app:layout_constraintBottom_toTopOf="@+id/composerLayout" /> - - - - - - - - - + + app:layout_constraintVertical_bias="1.0" + tools:visibility="gone" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/include_composer_layout.xml b/vector/src/main/res/layout/include_composer_layout.xml new file mode 100644 index 00000000..655691e9 --- /dev/null +++ b/vector/src/main/res/layout/include_composer_layout.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 17cb3d75..04c959ee 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -4,6 +4,7 @@ + diff --git a/vector/src/main/res/values/colors_riot.xml b/vector/src/main/res/values/colors_riot.xml index f60d4991..395ebbce 100644 --- a/vector/src/main/res/values/colors_riot.xml +++ b/vector/src/main/res/values/colors_riot.xml @@ -19,6 +19,7 @@ @color/accent_color_light #5EA584 #a6d0e5 + #81bddb diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index f816b9be..dabd32ae 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -27,6 +27,7 @@ @color/riot_primary_background_color_black @color/primary_color_black + #FFE9EDF1 @drawable/direct_chat_circle_black diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 5c1a506e..ba78e510 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -21,6 +21,7 @@ @color/riot_primary_background_color_dark @color/primary_color_dark + #FFE9EDF1 #55555555 diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 68076a4b..e0b58bf0 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -23,6 +23,7 @@ @color/riot_primary_background_color_light #FFF3F8FD + #FFE9EDF1 @style/Widget.Vector.Button diff --git a/vector/src/main/res/values/theme_status.xml b/vector/src/main/res/values/theme_status.xml index d2874e69..9a50b88d 100644 --- a/vector/src/main/res/values/theme_status.xml +++ b/vector/src/main/res/values/theme_status.xml @@ -23,6 +23,7 @@ @color/riot_primary_background_color_status @color/riot_primary_background_color_status + #FFE9EDF1 @style/Widget.Vector.Button From c6e428c047e2454b455a4a0645af8c76330ea5a0 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 23 May 2019 17:27:32 +0200 Subject: [PATCH 02/17] Fix / remove DebugActivity + clean --- .../im/vector/riotredesign/features/DebugActivity.kt | 12 ------------ .../features/home/room/detail/RoomDetailFragment.kt | 8 +++++--- 2 files changed, 5 insertions(+), 15 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotredesign/features/DebugActivity.kt diff --git a/vector/src/main/java/im/vector/riotredesign/features/DebugActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/DebugActivity.kt deleted file mode 100644 index 00708d03..00000000 --- a/vector/src/main/java/im/vector/riotredesign/features/DebugActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package im.vector.riotredesign.features - -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle - -class DebugActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_debug) - } -} diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 0ae0dda5..e34e9762 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -138,13 +138,12 @@ class RoomDetailFragment : * @return the sanitized display name */ fun sanitizeDisplayname(displayName: String): String? { - var displayName = displayName // sanity checks if (!TextUtils.isEmpty(displayName)) { val ircPattern = " (IRC)" if (displayName.endsWith(ircPattern)) { - displayName = displayName.substring(0, displayName.length - ircPattern.length) + return displayName.substring(0, displayName.length - ircPattern.length) } } @@ -202,7 +201,10 @@ class RoomDetailFragment : handleActions(it) }) - roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::selectedEvent, RoomDetailViewState::roomId) { mode, event, roomId -> + roomDetailViewModel.selectSubscribe( + RoomDetailViewState::sendMode, + RoomDetailViewState::selectedEvent, + RoomDetailViewState::roomId) { mode, event, roomId -> when (mode) { SendMode.REGULAR -> { val uid = session.sessionParams.credentials.userId From 1da0b5be76b88baf2a6ae702f4ba8528feae8dda Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 23 May 2019 17:53:11 +0200 Subject: [PATCH 03/17] Fix / Block command completion in Quote and Edit mode --- .../autocomplete/command/CommandAutocompletePolicy.kt | 6 ++++-- .../features/home/room/detail/RoomDetailFragment.kt | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandAutocompletePolicy.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandAutocompletePolicy.kt index 74ee50aa..38556cdb 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandAutocompletePolicy.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/command/CommandAutocompletePolicy.kt @@ -20,11 +20,13 @@ import android.text.Spannable import com.otaliastudios.autocomplete.AutocompletePolicy class CommandAutocompletePolicy : AutocompletePolicy { + + var enabled: Boolean = true + override fun getQuery(text: Spannable): CharSequence { if (text.length > 0) { return text.substring(1, text.length) } - // Should not happen return "" } @@ -34,7 +36,7 @@ class CommandAutocompletePolicy : AutocompletePolicy { // Only if text which starts with '/' and without space override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean { - return text?.startsWith("/") == true + return enabled && text?.startsWith("/") == true && !text.contains(" ") } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index e34e9762..89d76bf7 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -159,6 +159,7 @@ class RoomDetailFragment : private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() private val textComposerViewModel: TextComposerViewModel by fragmentViewModel() private val timelineEventController: TimelineEventController by inject { parametersOf(this) } + private val commandAutocompletePolicy = CommandAutocompletePolicy() private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) } private val autocompleteUserPresenter: AutocompleteUserPresenter by inject { parametersOf(this) } private val homePermalinkHandler: HomePermalinkHandler by inject() @@ -207,6 +208,7 @@ class RoomDetailFragment : RoomDetailViewState::roomId) { mode, event, roomId -> when (mode) { SendMode.REGULAR -> { + commandAutocompletePolicy.enabled = true val uid = session.sessionParams.credentials.userId val meMember = session.getRoom(roomId)?.getRoomMember(uid) AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composer_avatar_view) @@ -216,6 +218,7 @@ class RoomDetailFragment : } SendMode.EDIT, SendMode.QUOTE -> { + commandAutocompletePolicy.enabled = false if (event == null) { //we should ignore? can this happen? Timber.e("Enter edit mode with no event selected") @@ -321,7 +324,7 @@ class RoomDetailFragment : val elevation = 6f val backgroundDrawable = ColorDrawable(Color.WHITE) Autocomplete.on(composerEditText) - .with(CommandAutocompletePolicy()) + .with(commandAutocompletePolicy) .with(autocompleteCommandPresenter) .with(elevation) .with(backgroundDrawable) From 3c167017665909a1119f70bb0d1073ddd4581cf5 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 23 May 2019 17:56:05 +0200 Subject: [PATCH 04/17] Fix / line too long --- .../java/im/vector/riotredesign/core/utils/AnimationUtils.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt index a773991d..e15f18ee 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt @@ -9,7 +9,9 @@ import androidx.transition.Transition import androidx.transition.TransitionManager -inline fun ConstraintLayout.updateConstraintSet(@LayoutRes layoutId: Int, rootLayoutForAnimation: ConstraintLayout? = null, noinline onAnimationEnd: (() -> Unit)? = null) { +inline fun ConstraintLayout.updateConstraintSet(@LayoutRes layoutId: Int, + rootLayoutForAnimation: ConstraintLayout? = null, + noinline onAnimationEnd: (() -> Unit)? = null) { if (rootLayoutForAnimation != null) { val transition = ChangeBounds() transition.interpolator = OvershootInterpolator() From b45cc0e63fa5fa262580286f023e565b1351f8b7 Mon Sep 17 00:00:00 2001 From: Valere Date: Sat, 25 May 2019 14:49:35 +0200 Subject: [PATCH 05/17] Refactoring/ create custom view for composerLayout in timeline + simplify quote/edit composer preview animation --- .../riotredesign/core/utils/AnimationUtils.kt | 41 ------- .../home/room/detail/RoomDetailFragment.kt | 89 ++++++-------- .../room/detail/composer/TextComposerView.kt | 116 ++++++++++++++++++ .../timeline/action/MessageMenuViewModel.kt | 9 +- ...constraint_set_composer_layout_compact.xml | 17 ++- ...onstraint_set_composer_layout_expanded.xml | 41 +++++-- .../main/res/layout/fragment_room_detail.xml | 16 +-- ...r_layout.xml => merge_composer_layout.xml} | 18 +-- 8 files changed, 224 insertions(+), 123 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerView.kt rename vector/src/main/res/layout/{include_composer_layout.xml => merge_composer_layout.xml} (90%) diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt deleted file mode 100644 index e15f18ee..00000000 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt +++ /dev/null @@ -1,41 +0,0 @@ -package im.vector.riotredesign.core.utils - -import android.view.animation.OvershootInterpolator -import androidx.annotation.LayoutRes -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import androidx.transition.ChangeBounds -import androidx.transition.Transition -import androidx.transition.TransitionManager - - -inline fun ConstraintLayout.updateConstraintSet(@LayoutRes layoutId: Int, - rootLayoutForAnimation: ConstraintLayout? = null, - noinline onAnimationEnd: (() -> Unit)? = null) { - if (rootLayoutForAnimation != null) { - val transition = ChangeBounds() - transition.interpolator = OvershootInterpolator() - transition.addListener(object : Transition.TransitionListener { - override fun onTransitionResume(transition: Transition) { - } - - override fun onTransitionPause(transition: Transition) { - } - - override fun onTransitionCancel(transition: Transition) { - } - - override fun onTransitionStart(transition: Transition) { - } - - override fun onTransitionEnd(transition: Transition) { - onAnimationEnd?.invoke() - } - }) - TransitionManager.beginDelayedTransition(rootLayoutForAnimation, transition) - } - ConstraintSet().also { - it.clone(this@updateConstraintSet.context, layoutId) - it.applyTo(this@updateConstraintSet) - } -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 89d76bf7..30f5746e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -32,11 +32,9 @@ import android.view.HapticFeedbackConstants import android.view.LayoutInflater import android.view.View import android.view.inputmethod.InputMethodManager -import android.widget.ImageButton import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders @@ -79,6 +77,7 @@ 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.TextComposerView 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 @@ -96,7 +95,7 @@ import im.vector.riotredesign.features.media.VideoMediaViewerActivity import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* -import kotlinx.android.synthetic.main.include_composer_layout.* +import kotlinx.android.synthetic.main.merge_composer_layout.view.* import org.koin.android.ext.android.inject import org.koin.android.scope.ext.android.bindScope import org.koin.android.scope.ext.android.getOrCreateScope @@ -170,14 +169,8 @@ class RoomDetailFragment : private lateinit var actionViewModel: ActionsHandler - @BindView(R.id.composer_related_message_sender) - lateinit var composerRelatedMessageTitle: TextView - @BindView(R.id.composer_related_message_preview) - lateinit var composerRelatedMessageContent: TextView @BindView(R.id.composerLayout) - lateinit var composerLayout: ConstraintLayout - @BindView(R.id.rootConstraintLayout) - lateinit var rootConstraintLayout: ConstraintLayout + lateinit var composerLayout: TextComposerView override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) @@ -211,10 +204,8 @@ class RoomDetailFragment : commandAutocompletePolicy.enabled = true val uid = session.sessionParams.credentials.userId val meMember = session.getRoom(roomId)?.getRoomMember(uid) - AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composer_avatar_view) - composerLayout.updateConstraintSet(R.layout.constraint_set_composer_layout_compact, rootConstraintLayout) { - focusComposerAndShowKeyboard() - } + AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) + composerLayout.collapse() } SendMode.EDIT, SendMode.QUOTE -> { @@ -225,40 +216,37 @@ class RoomDetailFragment : return@selectSubscribe } //switch to expanded bar - composerRelatedMessageTitle.text = event.senderName - composerRelatedMessageTitle.setTextColor( - ContextCompat.getColor(requireContext(), AvatarRenderer.getColorFromUserId(event.root.sender - ?: "")) - ) + composerLayout.composerRelatedMessageTitle.apply { + text = event.senderName + setTextColor(ContextCompat.getColor(requireContext(), AvatarRenderer.getColorFromUserId(event.root.sender + ?: ""))) + } + //TODO this is used at several places, find way to refactor? val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() ?: event.root.content.toModel() val eventTextBody = messageContent?.body - composerRelatedMessageContent.text = eventTextBody + composerLayout.composerRelatedMessageContent.text = eventTextBody if (mode == SendMode.EDIT) { - composerEditText.setText(eventTextBody) - composer_related_message_action_image.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit)) + composerLayout.composerEditText.setText(eventTextBody) + composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit)) } else { - composerEditText.setText("") - composer_related_message_action_image.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_quote)) + composerLayout.composerEditText.setText("") + composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_quote)) } AvatarRenderer.render(event.senderAvatar, event.root.sender - ?: "", event.senderName, composer_avatar_view) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) - composerEditText.setSelection(composerEditText.text.length) - composerLayout.updateConstraintSet(R.layout.constraint_set_composer_layout_expanded, rootConstraintLayout) { + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) + composerLayout.expand { focusComposerAndShowKeyboard() } - - view?.findViewById(R.id.composer_related_message_close)?.setOnClickListener { - - composerRelatedMessageTitle.text = "" - composerRelatedMessageContent.text = "" - composerEditText.setText("") + composerLayout.composerRelatedMessageCloseButton.setOnClickListener { + composerLayout.composerEditText.setText("") roomDetailViewModel.resetSendMode() } @@ -323,7 +311,7 @@ class RoomDetailFragment : private fun setupComposer() { val elevation = 6f val backgroundDrawable = ColorDrawable(Color.WHITE) - Autocomplete.on(composerEditText) + Autocomplete.on(composerLayout.composerEditText) .with(commandAutocompletePolicy) .with(autocompleteCommandPresenter) .with(elevation) @@ -343,7 +331,7 @@ class RoomDetailFragment : .build() autocompleteUserPresenter.callback = this - Autocomplete.on(composerEditText) + Autocomplete.on(composerLayout.composerEditText) .with(CharPolicy('@', true)) .with(autocompleteUserPresenter) .with(elevation) @@ -371,7 +359,7 @@ class RoomDetailFragment : // Add the span val user = session.getUser(item.userId) val span = PillImageSpan(glideRequests, context!!, item.userId, user) - span.bind(composerEditText) + span.bind(composerLayout.composerEditText) editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -383,8 +371,8 @@ class RoomDetailFragment : }) .build() - sendButton.setOnClickListener { - val textMessage = composerEditText.text.toString() + composerLayout.sendButton.setOnClickListener { + val textMessage = composerLayout.composerEditText.text.toString() if (textMessage.isNotBlank()) { roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage)) } @@ -392,7 +380,7 @@ class RoomDetailFragment : } private fun setupAttachmentButton() { - attachmentButton.setOnClickListener { + composerLayout.attachmentButton.setOnClickListener { val intent = Intent(requireContext(), FilePickerActivity::class.java) intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder() .setCheckPermission(true) @@ -479,7 +467,7 @@ class RoomDetailFragment : val uid = session.sessionParams.credentials.userId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) - AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composer_avatar_view) + AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) } else if (summary?.membership == Membership.INVITE && inviter != null) { inviteView.visibility = View.VISIBLE @@ -511,7 +499,7 @@ class RoomDetailFragment : is SendMessageResult.MessageSent, is SendMessageResult.SlashCommandHandled -> { // Clear composer - composerEditText.text = null + composerLayout.composerEditText.text = null } is SendMessageResult.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) @@ -705,6 +693,7 @@ class RoomDetailFragment : * * @param text the text to insert. */ + //TODO legacy, refactor private fun insertUserDisplayNameInTextEditor(text: String?) { //TODO move logic outside of fragment if (null != text) { @@ -713,21 +702,21 @@ class RoomDetailFragment : val myDisplayName = session.getUser(session.sessionParams.credentials.userId)?.displayName if (TextUtils.equals(myDisplayName, text)) { // current user - if (TextUtils.isEmpty(composerEditText.text)) { - composerEditText.append(Command.EMOTE.command + " ") - composerEditText.setSelection(composerEditText.text.length) + 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(composerEditText.text)) { + if (TextUtils.isEmpty(composerLayout.composerEditText.text)) { // Ensure displayName will not be interpreted as a Slash command if (text.startsWith("/")) { - composerEditText.append("\\") + composerLayout.composerEditText.append("\\") } - composerEditText.append(sanitizeDisplayname(text)!! + ": ") + composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ") } else { - composerEditText.text.insert(composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ") + composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ") } // vibrate = true @@ -744,9 +733,9 @@ class RoomDetailFragment : } private fun focusComposerAndShowKeyboard() { - composerEditText.requestFocus() + composerLayout.composerEditText.requestFocus() val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.showSoftInput(composerEditText, InputMethodManager.SHOW_IMPLICIT) + imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT) } fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerView.kt new file mode 100644 index 00000000..c0fc2725 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerView.kt @@ -0,0 +1,116 @@ +package im.vector.riotredesign.features.home.room.detail.composer + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.EditText +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.view.isVisible +import androidx.transition.AutoTransition +import androidx.transition.Transition +import androidx.transition.TransitionManager +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.riotredesign.R + + +/** + * Encapsulate the timeline composer UX. + * + */ +class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, + defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) { + + @BindView(R.id.composer_related_message_sender) + lateinit var composerRelatedMessageTitle: TextView + @BindView(R.id.composer_related_message_preview) + lateinit var composerRelatedMessageContent: TextView + @BindView(R.id.composer_related_message_avatar_view) + lateinit var composerRelatedMessageAvatar: ImageView + @BindView(R.id.composer_related_message_action_image) + lateinit var composerRelatedMessageActionIcon: ImageView + @BindView(R.id.composer_related_message_close) + lateinit var composerRelatedMessageCloseButton: ImageButton + @BindView(R.id.composerEditText) + lateinit var composerEditText: EditText + @BindView(R.id.composer_avatar_view) + lateinit var composerAvatarImageView: ImageView + + var currentConstraintSetId: Int = -1 + + + init { + inflate(context, R.layout.merge_composer_layout, this) + ButterKnife.bind(this) + collapse(false) + } + + + fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { + if (currentConstraintSetId == R.layout.constraint_set_composer_layout_compact) { + //ignore we good + return + } + currentConstraintSetId = R.layout.constraint_set_composer_layout_compact + if (animate) { + val transition = AutoTransition() +// transition.duration = 5000 + transition.addListener(object : Transition.TransitionListener { + + override fun onTransitionEnd(transition: Transition) { + transitionComplete?.invoke() + } + + override fun onTransitionResume(transition: Transition) {} + + override fun onTransitionPause(transition: Transition) {} + + override fun onTransitionCancel(transition: Transition) {} + + override fun onTransitionStart(transition: Transition) {} + } + ) + TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition) + } + ConstraintSet().also { + it.clone(context, currentConstraintSetId) + it.applyTo(this) + } + } + + fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { + if (currentConstraintSetId == R.layout.constraint_set_composer_layout_expanded) { + //ignore we good + return + } + currentConstraintSetId = R.layout.constraint_set_composer_layout_expanded + if (animate) { + val transition = AutoTransition() +// transition.duration = 5000 + transition.addListener(object : Transition.TransitionListener { + + override fun onTransitionEnd(transition: Transition) { + transitionComplete?.invoke() + } + + override fun onTransitionResume(transition: Transition) {} + + override fun onTransitionPause(transition: Transition) {} + + override fun onTransitionCancel(transition: Transition) {} + + override fun onTransitionStart(transition: Transition) {} + } + ) + TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition) + } + ConstraintSet().also { + it.clone(context, currentConstraintSetId) + it.applyTo(this) + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index 4fee2f41..8d32a7a4 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -67,6 +67,13 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel().apply { + + if (event.sendState == SendState.SENDING) { + //TODO add cancel? + return@apply + } + //TODO is downloading attachement? + this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, event.root.eventId)) if (canCopy(type)) { //TODO copy images? html? see ClipBoard @@ -100,8 +107,6 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml index 9a354fc2..48048e62 100644 --- a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml +++ b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml @@ -22,9 +22,9 @@ android:layout_width="0dp" android:layout_height="1dp" android:background="?vctr_bottom_nav_background_border_color" - app:layout_constraintTop_toTopOf="@id/related_message_backround" app:layout_constraintEnd_toEndOf="@id/related_message_backround" - app:layout_constraintStart_toStartOf="@+id/related_message_backround" /> + app:layout_constraintStart_toStartOf="@+id/related_message_backround" + app:layout_constraintTop_toTopOf="@id/related_message_backround" /> + + @@ -68,9 +82,9 @@ android:alpha="1" android:tint="?android:attr/textColorTertiary" android:visibility="visible" - app:layout_constraintEnd_toEndOf="@id/composer_avatar_view" - app:layout_constraintStart_toStartOf="@id/composer_avatar_view" - app:layout_constraintTop_toBottomOf="@id/composer_avatar_view" + app:layout_constraintEnd_toEndOf="@id/composer_related_message_avatar_view" + app:layout_constraintStart_toStartOf="@id/composer_related_message_avatar_view" + app:layout_constraintTop_toBottomOf="@id/composer_related_message_avatar_view" tools:src="@drawable/ic_edit" /> @@ -90,16 +104,19 @@ @@ -149,7 +166,7 @@ android:textSize="14sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/sendButton" - app:layout_constraintStart_toStartOf="parent" + app:layout_constraintStart_toEndOf="@id/composer_avatar_view" app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier" tools:text="@tools:sample/lorem" /> diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 0f631722..970b7f4d 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -77,20 +77,20 @@ android:id="@+id/recyclerView" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintBottom_toTopOf="@+id/composerDivider" + app:layout_constraintBottom_toTopOf="@+id/composerLayout" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/toolbar" tools:listitem="@layout/item_timeline_event_text_message" /> - - + - + tools:constraintSet="@layout/constraint_set_composer_layout_compact" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> +// +// +// This is where the reply goes. + val body = bodyForReply(eventReplied.content.toModel()) + val replyFallbackTemplateFormatted = """ + +
+ ${stringProvider.getString(R.string.in_reply_to)} + %s +
+ %s +
+
+ %s + """.trim().format(permalink, userLink, userId, body.second ?: body.first, replyText) +// +// > <@alice:example.org> This is the original body +// +// This is where the reply goes + val lines = body.first.split("\n") + val plainTextBody = StringBuffer("><${userId}>") + lines.firstOrNull()?.also { plainTextBody.append(" $it") } + lines.forEachIndexed { index, s -> + if (index > 0) { + plainTextBody.append("\n>$s") + } + } + plainTextBody.append("\n\n").append(replyText) + + val eventId = eventReplied.eventId ?: return null + val content = MessageTextContent( + type = MessageType.MSGTYPE_TEXT, + format = MessageType.FORMAT_MATRIX_HTML, + body = plainTextBody.toString(), + formattedBody = replyFallbackTemplateFormatted, + relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId)) + ) + return createEvent(roomId, content) + } + + private fun bodyForReply(content: MessageContent?): Pair { + when (content?.type) { + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE -> { + //If we already have formatted body, return it? + var formattedText: String? = null + if (content is MessageTextContent) { + if (content.format == MessageType.FORMAT_MATRIX_HTML) { + formattedText = content.formattedBody + } + } + return content.body to formattedText + } + MessageType.MSGTYPE_FILE -> return stringProvider.getString(R.string.sent_a_file) to null + MessageType.MSGTYPE_AUDIO -> return stringProvider.getString(R.string.sent_an_audio_file) to null + MessageType.MSGTYPE_IMAGE -> return stringProvider.getString(R.string.sent_an_image) to null + MessageType.MSGTYPE_VIDEO -> return stringProvider.getString(R.string.sent_a_video) to null + else -> return (content?.body ?: "") to null + + } + + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 864ef13e..73330770 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.network.executeRequest @@ -57,6 +58,11 @@ internal class SendEventWorker(context: Context, params: WorkerParameters) localEvent.content ) } - return result.fold({ Result.retry() }, { Result.success() }) + return result.fold({ + when (it) { + is Failure.NetworkConnection -> Result.retry() + else -> Result.failure() + } + }, { Result.success() }) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt new file mode 100644 index 00000000..8b95a633 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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 + * + * 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. + */ + +package im.vector.matrix.android.internal.util + +import android.content.res.Resources +import androidx.annotation.NonNull +import androidx.annotation.StringRes + +class StringProvider(private val resources: Resources) { + + /** + * Returns a localized string from the application's package's + * default string table. + * + * @param resId Resource id for the string + * @return The string data associated with the resource, stripped of styled + * text information. + */ + @NonNull + fun getString(@StringRes resId: Int): String { + return resources.getString(resId) + } + + /** + * Returns a localized formatted string from the application's package's + * default string table, substituting the format arguments as defined in + * [java.util.Formatter] and [java.lang.String.format]. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for + * substitution. + * @return The string data associated with the resource, formatted and + * stripped of styled text information. + */ + @NonNull + fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { + return resources.getString(resId, *formatArgs) + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values/strings_riotX.xml b/matrix-sdk-android/src/main/res/values/strings_riotX.xml new file mode 100644 index 00000000..2fea241b --- /dev/null +++ b/matrix-sdk-android/src/main/res/values/strings_riotX.xml @@ -0,0 +1,9 @@ + + + + In reply to + sent a file. + sent an image. + sent a video. + sent an audio file. + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 1474d2da..4ccbb0ae 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -60,7 +60,7 @@ class HomeModule { val timelineDateFormatter = TimelineDateFormatter(get()) val timelineMediaSizeProvider = TimelineMediaSizeProvider() val colorProvider = ColorProvider(fragment.requireContext()) - val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer) + val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer,get()) val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory, roomNameItemFactory = RoomNameItemFactory(get()), diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 190c37de..7835c2da 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -38,6 +38,7 @@ sealed class RoomDetailActions { data class EnterEditMode(val eventId: String) : RoomDetailActions() data class EnterQuoteMode(val eventId: String) : RoomDetailActions() + data class EnterReplyMode(val eventId: String) : RoomDetailActions() } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 30f5746e..4b2a010d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -208,7 +208,8 @@ class RoomDetailFragment : composerLayout.collapse() } SendMode.EDIT, - SendMode.QUOTE -> { + SendMode.QUOTE, + SendMode.REPLY -> { commandAutocompletePolicy.enabled = false if (event == null) { //we should ignore? can this happen? @@ -233,9 +234,12 @@ class RoomDetailFragment : if (mode == SendMode.EDIT) { composerLayout.composerEditText.setText(eventTextBody) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit)) - } else { + } else if (mode == SendMode.QUOTE) { composerLayout.composerEditText.setText("") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_quote)) + } else if (mode == SendMode.REPLY) { + composerLayout.composerEditText.setText("") + composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_reply)) } AvatarRenderer.render(event.senderAvatar, event.root.sender @@ -673,13 +677,17 @@ class RoomDetailFragment : } } MessageMenuViewModel.ACTION_EDIT -> { - val eventId = actionData.data.toString() ?: return@let + val eventId = actionData.data.toString() roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId)) } MessageMenuViewModel.ACTION_QUOTE -> { - val eventId = actionData.data.toString() ?: return@let + val eventId = actionData.data.toString() roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(eventId)) } + MessageMenuViewModel.ACTION_REPLY -> { + val eventId = actionData.data.toString() + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } else -> { Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index f5664efc..40bb323d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -96,6 +96,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action) is RoomDetailActions.EnterEditMode -> handleEditAction(action) is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) } } @@ -208,24 +209,34 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) } SendMode.QUOTE -> { - withState { state -> - val messageContent: MessageContent? = - state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel() - ?: state.selectedEvent?.root?.content.toModel() - val textMsg = messageContent?.body + val messageContent: MessageContent? = + state.selectedEvent?.annotations?.editSummary?.aggregatedContent?.toModel() + ?: state.selectedEvent?.root?.content.toModel() + val textMsg = messageContent?.body - val finalText = legacyRiotQuoteText(textMsg, action.text) + val finalText = legacyRiotQuoteText(textMsg, action.text) - //TODO Refactor this, just temporary for quotes - val parser = Parser.builder().build() - val document = parser.parse(finalText) - val renderer = HtmlRenderer.builder().build() - val htmlText = renderer.render(document) - if (TextUtils.equals(finalText, htmlText)) { - room.sendTextMessage(finalText) - } else { - room.sendFormattedTextMessage(finalText, htmlText) - } + //TODO Refactor this, just temporary for quotes + val parser = Parser.builder().build() + val document = parser.parse(finalText) + val renderer = HtmlRenderer.builder().build() + val htmlText = renderer.render(document) + if (TextUtils.equals(finalText, htmlText)) { + room.sendTextMessage(finalText) + } else { + room.sendFormattedTextMessage(finalText, htmlText) + } + setState { + copy( + sendMode = SendMode.REGULAR, + selectedEvent = null + ) + } + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + } + SendMode.REPLY -> { + state.selectedEvent?.let { + room.replyToMessage(it.root, action.text) setState { copy( sendMode = SendMode.REGULAR, @@ -377,6 +388,17 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, } } } + private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) { + room.getTimeLineEvent(action.eventId)?.let { + setState { + copy( + sendMode = SendMode.REPLY, + selectedEvent = it + ) + } + } + } + private fun observeEventDisplayedActions() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt index 00ce0b47..c0a83ad2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt @@ -36,7 +36,8 @@ import im.vector.matrix.android.api.session.user.model.User enum class SendMode { REGULAR, QUOTE, - EDIT + EDIT, + REPLY } data class RoomDetailViewState( diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index a35199a0..9198f8ee 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -21,8 +21,14 @@ import com.airbnb.mvrx.ViewModelContext import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.riotredesign.core.platform.VectorViewModel +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer import org.koin.android.ext.android.get +import ru.noties.markwon.Markwon +import ru.noties.markwon.html.HtmlPlugin import timber.log.Timber import java.text.SimpleDateFormat import java.util.* @@ -31,7 +37,7 @@ import java.util.* data class MessageActionState( val userId: String, val senderName: String, - val messageBody: String, + val messageBody: CharSequence, val ts: String?, val senderAvatarPath: String? = null) : MvRxState @@ -54,10 +60,19 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() ?: event.root.content.toModel() val originTs = event.root.originServerTs + var body: CharSequence = messageContent?.body ?: "" + if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { + val parser = Parser.builder().build() + val document = parser.parse(messageContent.formattedBody ?: messageContent.body) + // val renderer = HtmlRenderer.builder().build() + body = Markwon.builder(viewModelContext.activity) + .usePlugin(HtmlPlugin.create()).build().render(document) +// body = renderer.render(document) + } MessageActionState( event.root.sender ?: "", parcel.informationData.memberName.toString(), - messageContent?.body ?: "", + body, dateFormat.format(Date(originTs ?: 0)), currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl) ) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index 8d32a7a4..d225b510 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -57,7 +57,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel + android:viewportHeight="13"> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 556bd50c..5f2aabe1 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -17,4 +17,7 @@ Event moderated by room admin Last edited by %s on %s + + Malformed event, cannot display + \ No newline at end of file From d9fecabc1ffff42566ff09df7e8c3ff69adbad76 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 27 May 2019 12:30:05 +0200 Subject: [PATCH 07/17] Fix / Edits could break cells merging Edits are not displayable --- .../timeline/factory/MessageItemFactory.kt | 4 +-- .../helper/TimelineDisplayableEvents.kt | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 6898878b..05fda074 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -105,10 +105,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider, ?: //Malformed content, we should echo something on screen return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) - //TODO this should be filtered as not displayable? if (messageContent.relatesTo?.type == RelationType.REPLACE) { - //TODO blank item or ignore?? - // ignore this event + // ignore replace event, the targeted id is already edited return BlankItem_() } // val all = event.root.toContent() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 53d63f58..91c39e38 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -17,6 +17,10 @@ package im.vector.riotredesign.features.home.room.detail.timeline.helper import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.RelationType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.core.extensions.localDateTime @@ -40,14 +44,25 @@ object TimelineDisplayableEvents { } fun TimelineEvent.isDisplayable(): Boolean { - return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type) && !root.content.isNullOrEmpty() -} - -fun List.filterDisplayableEvents(): List { - return this.filter { - it.isDisplayable() + if (!TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type)) { + return false } + if (root.content.isNullOrEmpty()) { + return false + } + //Edits should be filtered out! + if (EventType.MESSAGE == root.type + && root.content.toModel()?.relatesTo?.type == RelationType.REPLACE) { + return false + } + return true } +// +//fun List.filterDisplayableEvents(): List { +// return this.filter { +// it.isDisplayable() +// } +//} fun TimelineEvent.canBeMerged(): Boolean { return root.type == EventType.STATE_ROOM_MEMBER From c7c721197876bd1d170d27c0c3fc48878d3be46c Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 27 May 2019 17:02:12 +0200 Subject: [PATCH 08/17] Fix layout preview --- vector/src/main/res/layout/fragment_room_detail.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 970b7f4d..2f086e13 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -81,7 +81,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/toolbar" - tools:listitem="@layout/item_timeline_event_text_message" /> + tools:listitem="@layout/item_timeline_event_base" /> Date: Mon, 27 May 2019 18:08:29 +0200 Subject: [PATCH 09/17] Added auto markdown (as per preference) Fix / show formatted message preview upon composer in edit/quote/reply Fix / use aggregated content to decide for actions on long click --- matrix-sdk-android/build.gradle | 3 ++ .../room/model/annotation/RelationService.kt | 2 +- .../api/session/room/send/SendService.kt | 2 +- .../room/annotation/DefaultRelationService.kt | 4 +- .../session/room/send/DefaultSendService.kt | 4 +- .../room/send/LocalEchoEventFactory.kt | 41 ++++++++++++++++--- .../home/room/detail/RoomDetailActions.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 20 +++++++-- .../home/room/detail/RoomDetailViewModel.kt | 4 +- .../timeline/action/MessageMenuViewModel.kt | 3 +- 10 files changed, 65 insertions(+), 20 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index c04907ab..5211a223 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -91,6 +91,7 @@ dependencies { def moshi_version = '1.8.0' def lifecycle_version = '2.0.0' def coroutines_version = "1.0.1" + def markwon_version = '3.0.0-SNAPSHOT' implementation fileTree(dir: 'libs', include: ['*.aar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -112,6 +113,8 @@ dependencies { implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" + implementation "ru.noties.markwon:core:$markwon_version" + // Database implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1' diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt index 5185babf..5838aef7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt @@ -60,7 +60,7 @@ interface RelationService { * @param newBodyText The edited body * @param compatibilityBodyText The text that will appear on clients that don't support yet edition */ - fun editTextMessage(targetEventId: String, newBodyText: String, compatibilityBodyText: String = "* $newBodyText"): Cancelable + fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): Cancelable fun replyToMessage(eventReplied: Event, replyText: String) : Cancelable? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 551ab545..f6f4708a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -33,7 +33,7 @@ interface SendService { * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @return a [Cancelable] */ - fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable + fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable fun sendFormattedTextMessage(text: String,formattedText: String): Cancelable /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt index ec36f683..5cd1873c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt @@ -160,8 +160,8 @@ internal class DefaultRelationService(private val roomId: String, .build() } - override fun editTextMessage(targetEventId: String, newBodyText: String, compatibilityBodyText: String): Cancelable { - val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, MessageType.MSGTYPE_TEXT, compatibilityBodyText) + override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable { + val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText) val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 1d6197a3..6b16ffaf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -49,8 +49,8 @@ internal class DefaultSendService(private val roomId: String, : SendService { - override fun sendTextMessage(text: String, msgType: String): Cancelable { - val event = eventFactory.createTextEvent(roomId, msgType, text).also { + override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable { + val event = eventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { saveLocalEcho(it) } val sendWork = createSendEventWork(event) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 6b1cdb11..a7b261de 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever +import android.text.TextUtils import im.vector.matrix.android.R import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.permalinks.PermalinkFactory @@ -29,10 +30,21 @@ import im.vector.matrix.android.api.session.room.model.annotation.ReplyToContent import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.util.StringProvider +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) { - fun createTextEvent(roomId: String, msgType: String, text: String): Event { + fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { + if (autoMarkdown && msgType == MessageType.MSGTYPE_TEXT) { + val parser = Parser.builder().build() + val document = parser.parse(text) + val renderer = HtmlRenderer.builder().build() + val htmlText = renderer.render(document) + if (!TextUtils.equals(text, htmlText)) { + return createFormattedTextEvent(roomId, text, htmlText) + } + } val content = MessageTextContent(type = msgType, body = text) return createEvent(roomId, content) } @@ -48,15 +60,32 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva } - fun createReplaceTextEvent(roomId: String, targetEventId: String, newBodyText: String, msgType: String, compatibilityText: String): Event { + fun createReplaceTextEvent(roomId: String, targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, msgType: String, compatibilityText: String): Event { + + var newContent = MessageTextContent( + type = MessageType.MSGTYPE_TEXT, + body = newBodyText + ) + if (newBodyAutoMarkdown) { + val parser = Parser.builder().build() + val document = parser.parse(newBodyText) + val renderer = HtmlRenderer.builder().build() + val htmlText = renderer.render(document) + if (!TextUtils.equals(newBodyText, htmlText)) { + newContent = MessageTextContent( + type = MessageType.MSGTYPE_TEXT, + format = MessageType.FORMAT_MATRIX_HTML, + body = newBodyText, + formattedBody = htmlText + ) + } + } + val content = MessageTextContent( type = msgType, body = compatibilityText, relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), - newContent = MessageTextContent( - type = MessageType.MSGTYPE_TEXT, - body = newBodyText - ).toContent() + newContent = newContent.toContent() ) return createEvent(roomId, content) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 7835c2da..3ab393a0 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent sealed class RoomDetailActions { - data class SendMessage(val text: String) : RoomDetailActions() + data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : RoomDetailActions() object IsDisplayed : RoomDetailActions() data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 4b2a010d..ef930771 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -93,13 +93,17 @@ import im.vector.riotredesign.features.media.ImageMediaViewerActivity import im.vector.riotredesign.features.media.VideoContentRenderer import im.vector.riotredesign.features.media.VideoMediaViewerActivity import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity +import im.vector.riotredesign.features.settings.PreferencesManager import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.merge_composer_layout.view.* +import org.commonmark.parser.Parser 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 ru.noties.markwon.Markwon +import ru.noties.markwon.html.HtmlPlugin import timber.log.Timber import java.io.File @@ -227,12 +231,20 @@ class RoomDetailFragment : val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() ?: event.root.content.toModel() - val eventTextBody = messageContent?.body - composerLayout.composerRelatedMessageContent.text = eventTextBody + val nonFormattedBody = messageContent?.body ?: "" + var formattedBody: CharSequence? = null + if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { + val parser = Parser.builder().build() + val document = parser.parse(messageContent.formattedBody ?: messageContent.body) + formattedBody = Markwon.builder(requireContext()) + .usePlugin(HtmlPlugin.create()).build().render(document) + } + composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody if (mode == SendMode.EDIT) { - composerLayout.composerEditText.setText(eventTextBody) + //TODO if it's a reply we should trim the top part of message + composerLayout.composerEditText.setText(nonFormattedBody) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit)) } else if (mode == SendMode.QUOTE) { composerLayout.composerEditText.setText("") @@ -378,7 +390,7 @@ class RoomDetailFragment : composerLayout.sendButton.setOnClickListener { val textMessage = composerLayout.composerEditText.text.toString() if (textMessage.isNotBlank()) { - roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage)) + roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, PreferencesManager.isMarkdownEnabled(requireContext()))) } } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 40bb323d..bbb1baa2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -138,7 +138,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, when (slashCommandResult) { is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room - room.sendTextMessage(action.text) + room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) } is ParsedCommand.ErrorSyntax -> { @@ -199,7 +199,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, } } SendMode.EDIT -> { - room.editTextMessage(state?.selectedEvent?.root?.eventId ?: "", action.text) + room.editTextMessage(state.selectedEvent?.root?.eventId ?: "", action.text, action.autoMarkdown) setState { copy( sendMode = SendMode.REGULAR, diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index d225b510..86ddf866 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -50,7 +50,8 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel Date: Mon, 27 May 2019 18:21:59 +0200 Subject: [PATCH 10/17] Fix / avoid newlines and ws in reply event representations --- .../session/room/send/LocalEchoEventFactory.kt | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index a7b261de..96237a06 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -231,16 +231,8 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva // This is where the reply goes. val body = bodyForReply(eventReplied.content.toModel()) val replyFallbackTemplateFormatted = """ - -
- ${stringProvider.getString(R.string.in_reply_to)} - %s -
- %s -
-
- %s - """.trim().format(permalink, userLink, userId, body.second ?: body.first, replyText) +
${stringProvider.getString(R.string.in_reply_to)}%s
%s
%s + """.trimIndent().format(permalink, userLink, userId, body.second ?: body.first, replyText) // // > <@alice:example.org> This is the original body // From 20e903914cccf2d7186d114bb6629fbd32b547db Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 28 May 2019 10:43:36 +0200 Subject: [PATCH 11/17] Cleaning / code review --- .../room/model/annotation/RelationService.kt | 34 ++++++++++++++++--- .../api/session/room/send/SendService.kt | 13 +++++++ .../room/annotation/DefaultRelationService.kt | 6 +--- .../room/send/LocalEchoEventFactory.kt | 10 +++--- .../android/internal/util/StringProvider.kt | 2 +- .../src/main/res/values/strings_riotX.xml | 9 ----- 6 files changed, 50 insertions(+), 24 deletions(-) delete mode 100644 matrix-sdk-android/src/main/res/values/strings_riotX.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt index 5838aef7..40b6fb2d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt @@ -16,11 +16,31 @@ package im.vector.matrix.android.api.session.room.model.annotation import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable -//TODO rename in relationService? +/** + * In some cases, events may wish to reference other events. + * This could be to form a thread of messages for the user to follow along with, + * or to provide more context as to what a particular event is describing. + * Relation are used to associate new information with an existing event. + * + * Relations are events which have an m.relates_to mixin in their contents, + * and the new information they convey is expressed in their usual event type and content. + * + * Three types of relations are defined, each defining different behaviour when aggregated: + * + * m.annotation - lets you define an event which annotates an existing event. + * When aggregated, groups events together based on key and returns a count. + * (aka SQL's COUNT) These are primarily intended for handling reactions. + * + * m.replace - lets you define an event which replaces an existing event. + * When aggregated, returns the most recent replacement event. (aka SQL's MAX) + * These are primarily intended for handling edits. + * + * m.reference - lets you define an event which references an existing event. + * When aggregated, currently doesn't do anything special, but in future could bundle chains of references (i.e. threads). + * These are primarily intended for handling replies (and in future threads). + */ interface RelationService { @@ -63,6 +83,12 @@ interface RelationService { fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): Cancelable - fun replyToMessage(eventReplied: Event, replyText: String) : Cancelable? + /** + * Reply to an event in the timeline (must be in same room) + * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 + * @param eventReplied the event referenced by the reply + * @param replyText the reply text + */ + fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index f6f4708a..875ac75b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -31,9 +31,17 @@ interface SendService { * Method to send a text message asynchronously. * @param text the text message to send * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present * @return a [Cancelable] */ fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable + + /** + * Method to send a text message with a formatted body. + * @param text the text message to send + * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML + * @return a [Cancelable] + */ fun sendFormattedTextMessage(text: String,formattedText: String): Cancelable /** @@ -50,6 +58,11 @@ interface SendService { */ fun sendMedias(attachments: List): Cancelable + /** + * Redacts (delete) the given event. + * @param event The event to redact + * @param reason Optional reason string + */ fun redactEvent(event: Event, reason: String?): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt index 5cd1873c..c231ca52 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt @@ -179,11 +179,7 @@ internal class DefaultRelationService(private val roomId: String, } - /** - * Reply to an event in the timeline - * Users may wish to reference another message when forming their own message - * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 - */ + override fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? { val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText)?.also { saveLocalEcho(it) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 96237a06..4fab8aa5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -231,7 +231,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva // This is where the reply goes. val body = bodyForReply(eventReplied.content.toModel()) val replyFallbackTemplateFormatted = """ -
${stringProvider.getString(R.string.in_reply_to)}%s
%s
%s +
${stringProvider.getString(R.string.message_reply_to_prefix)}%s
%s
%s """.trimIndent().format(permalink, userLink, userId, body.second ?: body.first, replyText) // // > <@alice:example.org> This is the original body @@ -272,10 +272,10 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva } return content.body to formattedText } - MessageType.MSGTYPE_FILE -> return stringProvider.getString(R.string.sent_a_file) to null - MessageType.MSGTYPE_AUDIO -> return stringProvider.getString(R.string.sent_an_audio_file) to null - MessageType.MSGTYPE_IMAGE -> return stringProvider.getString(R.string.sent_an_image) to null - MessageType.MSGTYPE_VIDEO -> return stringProvider.getString(R.string.sent_a_video) to null + MessageType.MSGTYPE_FILE -> return stringProvider.getString(R.string.reply_to_a_file) to null + MessageType.MSGTYPE_AUDIO -> return stringProvider.getString(R.string.reply_to_an_audio_file) to null + MessageType.MSGTYPE_IMAGE -> return stringProvider.getString(R.string.reply_to_an_image) to null + MessageType.MSGTYPE_VIDEO -> return stringProvider.getString(R.string.reply_to_a_video) to null else -> return (content?.body ?: "") to null } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt index 8b95a633..02d90c2f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt @@ -20,7 +20,7 @@ import android.content.res.Resources import androidx.annotation.NonNull import androidx.annotation.StringRes -class StringProvider(private val resources: Resources) { +internal class StringProvider(private val resources: Resources) { /** * Returns a localized string from the application's package's diff --git a/matrix-sdk-android/src/main/res/values/strings_riotX.xml b/matrix-sdk-android/src/main/res/values/strings_riotX.xml deleted file mode 100644 index 2fea241b..00000000 --- a/matrix-sdk-android/src/main/res/values/strings_riotX.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - In reply to - sent a file. - sent an image. - sent a video. - sent an audio file. - \ No newline at end of file From 3cb99ff64f95d2ec5ed3e649ef4700a07a3e81ad Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 28 May 2019 10:45:58 +0200 Subject: [PATCH 12/17] Renamed package .internal.session.room.annotation to xx.relation --- .../im/vector/matrix/android/api/session/room/Room.kt | 2 +- .../session/room/model/message/MessageAudioContent.kt | 2 +- .../api/session/room/model/message/MessageContent.kt | 2 +- .../session/room/model/message/MessageDefaultContent.kt | 2 +- .../session/room/model/message/MessageEmoteContent.kt | 2 +- .../api/session/room/model/message/MessageFileContent.kt | 2 +- .../session/room/model/message/MessageImageContent.kt | 2 +- .../session/room/model/message/MessageLocationContent.kt | 2 +- .../session/room/model/message/MessageNoticeContent.kt | 2 +- .../api/session/room/model/message/MessageTextContent.kt | 2 +- .../session/room/model/message/MessageVideoContent.kt | 2 +- .../model/{annotation => relation}/ReactionContent.kt | 2 +- .../room/model/{annotation => relation}/ReactionInfo.kt | 2 +- .../model/{annotation => relation}/RelationContent.kt | 2 +- .../{annotation => relation}/RelationDefaultContent.kt | 2 +- .../model/{annotation => relation}/RelationService.kt | 2 +- .../model/{annotation => relation}/ReplyToContent.kt | 2 +- .../matrix/android/internal/session/room/DefaultRoom.kt | 2 +- .../session/room/EventRelationsAggregationUpdater.kt | 2 +- .../matrix/android/internal/session/room/RoomFactory.kt | 6 +++--- .../matrix/android/internal/session/room/RoomModule.kt | 9 ++++----- .../{annotation => relation}/DefaultRelationService.kt | 4 ++-- .../FindReactionEventForUndoTask.kt | 2 +- .../room/{annotation => relation}/SendRelationWorker.kt | 6 +++--- .../{annotation => relation}/UpdateQuickReactionTask.kt | 2 +- .../internal/session/room/send/LocalEchoEventFactory.kt | 8 ++++---- 26 files changed, 37 insertions(+), 38 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/{annotation => relation}/ReactionContent.kt (75%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/{annotation => relation}/ReactionInfo.kt (86%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/{annotation => relation}/RelationContent.kt (62%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/{annotation => relation}/RelationDefaultContent.kt (93%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/{annotation => relation}/RelationService.kt (98%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/{annotation => relation}/ReplyToContent.kt (92%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/{annotation => relation}/DefaultRelationService.kt (98%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/{annotation => relation}/FindReactionEventForUndoTask.kt (97%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/{annotation => relation}/SendRelationWorker.kt (92%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/{annotation => relation}/UpdateQuickReactionTask.kt (98%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 1e05d71a..86965ed4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room import androidx.lifecycle.LiveData import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.android.api.session.room.model.annotation.RelationService +import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt index f1df2b58..9b33b007 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageAudioContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt index efc9af06..c45e47fc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.room.model.message import im.vector.matrix.android.api.session.events.model.Content -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent interface MessageContent { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt index 4fb96a83..45ce9542 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageDefaultContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt index 8053fde4..88fd3bc1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageEmoteContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt index 57039d64..8f58294c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageFileContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt index 394b9879..2c978b97 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageImageContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt index d3a81a93..ddd67af9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageLocationContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt index 53adaf9b..54037c60 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageNoticeContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt index 7942c259..3256d830 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageTextContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt index 0ca7def1..40c29942 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.events.model.Content -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageVideoContent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionContent.kt similarity index 75% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionContent.kt index 02d4164d..c7a86631 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionContent.kt @@ -1,4 +1,4 @@ -package im.vector.matrix.android.api.session.room.model.annotation +package im.vector.matrix.android.api.session.room.model.relation import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt similarity index 86% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionInfo.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt index dcb7328d..3a2f6169 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt @@ -1,4 +1,4 @@ -package im.vector.matrix.android.api.session.room.model.annotation +package im.vector.matrix.android.api.session.room.model.relation import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt similarity index 62% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt index 3327fc45..3f60af7c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt @@ -1,4 +1,4 @@ -package im.vector.matrix.android.api.session.room.model.annotation +package im.vector.matrix.android.api.session.room.model.relation interface RelationContent { val type: String? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt similarity index 93% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationDefaultContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt index 5ccfca11..853a3817 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.api.session.room.model.annotation +package im.vector.matrix.android.api.session.room.model.relation import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt similarity index 98% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index 40b6fb2d..bc924728 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.api.session.room.model.annotation +package im.vector.matrix.android.api.session.room.model.relation import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.util.Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt similarity index 92% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReplyToContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt index 54226545..3df8a534 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReplyToContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.room.model.annotation +package im.vector.matrix.android.api.session.room.model.relation import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 7ee1ea28..fe425a6a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -22,7 +22,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.matrix.android.api.session.room.model.annotation.RelationService +import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index 08698c08..64802553 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.* -import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent +import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.EventMapper diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 1b544c00..e8029c09 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -18,9 +18,9 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.Room -import im.vector.matrix.android.internal.session.room.annotation.DefaultRelationService -import im.vector.matrix.android.internal.session.room.annotation.FindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.annotation.UpdateQuickReactionTask +import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService +import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index b1799bd2..7013b99d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -17,10 +17,10 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.internal.session.DefaultSession -import im.vector.matrix.android.internal.session.room.annotation.DefaultFindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.annotation.DefaultUpdateQuickReactionTask -import im.vector.matrix.android.internal.session.room.annotation.FindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.annotation.UpdateQuickReactionTask +import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask +import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask @@ -39,7 +39,6 @@ import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.timeline.* -import im.vector.matrix.android.internal.util.StringProvider import org.koin.dsl.module.module import retrofit2.Retrofit diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt similarity index 98% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index c231ca52..0c2addd5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.annotation +package im.vector.matrix.android.internal.session.room.relation import androidx.work.* import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.annotation.RelationService +import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.helper.addSendingEvent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/FindReactionEventForUndoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt similarity index 97% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/FindReactionEventForUndoTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt index 441b289e..0f7e25ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/FindReactionEventForUndoTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.annotation +package im.vector.matrix.android.internal.session.room.relation import arrow.core.Try import com.zhuinden.monarchy.Monarchy diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt similarity index 92% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/SendRelationWorker.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt index e262dcaf..8fac4ed2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.annotation +package im.vector.matrix.android.internal.session.room.relation import android.content.Context import androidx.work.Worker @@ -22,8 +22,8 @@ import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent -import im.vector.matrix.android.api.session.room.model.annotation.ReactionInfo +import im.vector.matrix.android.api.session.room.model.relation.ReactionContent +import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/UpdateQuickReactionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt similarity index 98% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/UpdateQuickReactionTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt index 56a8545b..7a28683a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/UpdateQuickReactionTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.annotation +package im.vector.matrix.android.internal.session.room.relation import arrow.core.Try import com.zhuinden.monarchy.Monarchy diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 4fab8aa5..886899ea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -23,10 +23,10 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.* -import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent -import im.vector.matrix.android.api.session.room.model.annotation.ReactionInfo -import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent -import im.vector.matrix.android.api.session.room.model.annotation.ReplyToContent +import im.vector.matrix.android.api.session.room.model.relation.ReactionContent +import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.util.StringProvider From deba7565982e17262334f26dea4b5633029917fd Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 28 May 2019 10:51:02 +0200 Subject: [PATCH 13/17] Added doc --- .../internal/session/room/send/LocalEchoEventFactory.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 886899ea..a810baac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -258,6 +258,10 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva return createEvent(roomId, content) } + /** + * Returns a pair of used for the fallback event representation + * in a reply message. + */ private fun bodyForReply(content: MessageContent?): Pair { when (content?.type) { MessageType.MSGTYPE_EMOTE, From 466be1dca5e26bbf442625b33655137fcd6d10e7 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 28 May 2019 15:17:06 +0200 Subject: [PATCH 14/17] Fix / issue with avatar url --- .../session/content/DefaultContentUrlResolver.kt | 16 ++++++++++++++-- .../internal/session/content/FileUploader.kt | 12 ++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt index 8845c372..0fad40d0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt @@ -21,10 +21,19 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver private const val MATRIX_CONTENT_URI_SCHEME = "mxc://" -internal const val URI_PREFIX_CONTENT_API = "_matrix/media/v1/" +private const val URI_PREFIX_CONTENT_API = "_matrix/media/v1/" internal class DefaultContentUrlResolver(private val homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver { + companion object { + fun getUploadUrl(homeServerConnectionConfig: HomeServerConnectionConfig): String { + val baseUrl = homeServerConnectionConfig.homeServerUri.toString() + val sep = if (baseUrl.endsWith("/")) "" else "/" + + return baseUrl + sep + URI_PREFIX_CONTENT_API + "upload" + } + } + override fun resolveFullSize(contentUrl: String?): String? { if (contentUrl?.isValidMatrixContentUrl() == true) { val baseUrl = homeServerConnectionConfig.homeServerUri.toString() @@ -57,7 +66,10 @@ internal class DefaultContentUrlResolver(private val homeServerConnectionConfig: fragment = serverAndMediaId.substring(fragmentOffset) serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset) } - return baseUrl + prefix + serverAndMediaId + (params ?: "") + fragment + + val sep = if (baseUrl.endsWith("/")) "" else "/" + + return baseUrl + sep + prefix + serverAndMediaId + (params ?: "") + fragment } private fun String.isValidMatrixContentUrl(): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 1ec18127..e8133070 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -21,19 +21,15 @@ import arrow.core.Try.Companion.raise import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.network.ProgressRequestBody -import okhttp3.HttpUrl -import okhttp3.MediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody +import okhttp3.* import java.io.File import java.io.IOException internal class FileUploader(private val okHttpClient: OkHttpClient, - private val sessionParams: SessionParams) { + sessionParams: SessionParams) { - private val uploadUrl = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload" + private val uploadUrl = DefaultContentUrlResolver.getUploadUrl(sessionParams.homeServerConnectionConfig) private val moshi = MoshiProvider.providesMoshi() private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) @@ -82,7 +78,7 @@ internal class FileUploader(private val okHttpClient: OkHttpClient, response.body()?.source()?.let { responseAdapter.fromJson(it) } - ?: throw IOException() + ?: throw IOException() } } } From 99925d7cf97fec6d5f0192a773ae24bee65a7fd9 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 29 May 2019 18:43:33 +0200 Subject: [PATCH 15/17] Local echo for reactions/edits/redacts --- matrix-sdk-android/build.gradle | 2 +- .../session/room/timeline/TimelineTest.kt | 5 +- .../room/model/EditAggregatedSummary.kt | 1 + .../room/model/ReactionAggregatedSummary.kt | 3 +- .../mapper/EventAnnotationsSummaryMapper.kt | 4 +- .../model/EditAggregatedSummaryEntity.kt | 1 + .../model/ReactionAggregatedSummaryEntity.kt | 4 +- .../database/query/EventEntityQueries.kt | 29 +- .../android/internal/session/SessionModule.kt | 10 +- .../room/EventRelationsAggregationTask.kt | 309 ++++++++++++++++++ .../room/EventRelationsAggregationUpdater.kt | 222 ++----------- .../internal/session/room/RoomModule.kt | 16 +- .../session/room/prune/EventsPruner.kt | 7 +- .../session/room/prune/PruneEventTask.kt | 30 +- .../room/relation/DefaultRelationService.kt | 111 +++---- .../room/relation/SendRelationWorker.kt | 6 +- .../session/room/send/DefaultSendService.kt | 50 +-- .../room/send/LocalEchoEventFactory.kt | 86 ++++- .../session/room/send/RedactEventWorker.kt | 20 +- .../session/room/send/SendEventWorker.kt | 6 +- .../timeline/TimelineSendEventWorkCommon.kt | 56 ++++ .../room/timeline/TokenChunkEventPersistor.kt | 18 +- .../internal/session/sync/RoomSyncHandler.kt | 24 +- .../internal/session/sync/SyncModule.kt | 2 +- .../timeline/factory/MessageItemFactory.kt | 4 +- .../detail/timeline/item/AbsMessageItem.kt | 9 +- .../timeline/item/MessageInformationData.kt | 16 +- 27 files changed, 658 insertions(+), 393 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 5211a223..1a0c0f91 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -48,7 +48,7 @@ android { buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" // Set to BODY instead of NONE to enable logging - buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BASIC" } release { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index e81e481d..221887c7 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -18,12 +18,10 @@ package im.vector.matrix.android.session.room.timeline import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.session.room.EventRelationExtractor -import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory @@ -60,8 +58,7 @@ internal class TimelineTest : InstrumentedTest { private fun createTimeline(initialEventId: String? = null): Timeline { val taskExecutor = TaskExecutor(testCoroutineDispatchers) - val erau = EventRelationsAggregationUpdater(Credentials("", "", "", null, null)) - val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy, erau) + val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt index 636343a5..759ba1ec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt @@ -21,5 +21,6 @@ data class EditAggregatedSummary( val aggregatedContent: Content? = null, // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) val sourceEvents: List, + val localEchos: List, val lastEditTs: Long = 0 ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt index a6948cd1..1fbe58c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt @@ -5,5 +5,6 @@ data class ReactionAggregatedSummary( val count: Int, // 8 val addedByMe: Boolean, // true val firstTimestamp: Long, // unix timestamp - val sourceEvents: List + val sourceEvents: List, + val localEchoEvents: List ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt index 2d1a7da1..8b653b74 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -15,13 +15,15 @@ internal object EventAnnotationsSummaryMapper { it.count, it.addedByMe, it.firstTimestamp, - it.sourceEvents.toList() + it.sourceEvents.toList(), + it.sourceLocalEcho.toList() ) }, editSummary = annotationsSummary.editSummary?.let { EditAggregatedSummary( ContentMapper.map(it.aggregatedContent), it.sourceEvents.toList(), + it.sourceLocalEchoEvents.toList(), it.lastEditTs ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt index 4b2a43e8..f2690c96 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt @@ -25,6 +25,7 @@ internal open class EditAggregatedSummaryEntity( var aggregatedContent: String? = null, // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) var sourceEvents: RealmList = RealmList(), + var sourceLocalEchoEvents: RealmList = RealmList(), var lastEditTs: Long = 0 ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt index 93ec8bd7..4b94313d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt @@ -16,7 +16,9 @@ internal open class ReactionAggregatedSummaryEntity( // The first time this reaction was added (for ordering purpose) var firstTimestamp: Long = 0, // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) - var sourceEvents: RealmList = RealmList() + var sourceEvents: RealmList = RealmList(), + // List of transaction ids for local echos + var sourceLocalEcho: RealmList = RealmList() ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index c500a880..c366ff75 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.database.query +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.* @@ -42,12 +43,36 @@ internal fun EventEntity.Companion.where(realm: Realm, query.equalTo(EventEntityFields.TYPE, type) } return when (linkFilterMode) { - LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false) + LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false) UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true) - BOTH -> query + BOTH -> query } } +//internal fun EventEntity.Companion.unsent(realm: Realm, +// roomId: String? = null): RealmQuery { +// val query = realm.where() +// if (roomId != null) { +// query.equalTo(EventEntityFields.ROOM_ID, roomId) +// } +// query.equalTo(EventEntityFields.SEND_STATE_STR, SendState.UNSENT.name) +// return query +//} +// +//internal fun EventEntity.Companion.byTypes(realm: Realm, +// types: List): RealmQuery { +// val query = realm.where() +// types.forEachIndexed { index, type -> +// if (index == 0) { +// query.equalTo(EventEntityFields.TYPE, type) +// } else { +// query.or().equalTo(EventEntityFields.TYPE, type) +// } +// } +// return query +//} + + internal fun EventEntity.Companion.latestEvent(realm: Realm, roomId: String, includedTypes: List = emptyList(), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 6d8084c0..8f8ee454 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -105,10 +105,6 @@ internal class SessionModule(private val sessionParams: SessionParams) { RoomSummaryUpdater(get(), get(), get()) } - scope(DefaultSession.SCOPE) { - EventRelationsAggregationUpdater(get()) - } - scope(DefaultSession.SCOPE) { DefaultRoomService(get(), get(), get(), get()) as RoomService } @@ -168,9 +164,11 @@ internal class SessionModule(private val sessionParams: SessionParams) { scope(DefaultSession.SCOPE) { val groupSummaryUpdater = GroupSummaryUpdater(get()) - val eventsPruner = EventsPruner(get(), get(), get(), get()) val userEntityUpdater = UserEntityUpdater(get(), get(), get()) - listOf(groupSummaryUpdater, eventsPruner, userEntityUpdater) + val aggregationUpdater = EventRelationsAggregationUpdater(get(), get(), get(), get()) + //Event pruner must be the last one, because it will clear contents + val eventsPruner = EventsPruner(get(), get(), get(), get()) + listOf(groupSummaryUpdater, userEntityUpdater, aggregationUpdater, eventsPruner) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt new file mode 100644 index 00000000..5181bb30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -0,0 +1,309 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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 + * + * 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. + */ +package im.vector.matrix.android.internal.session.room + +import arrow.core.Try +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.* +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.relation.ReactionContent +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.mapper.EventMapper +import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.query.create +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.tryTransactionAsync +import io.realm.Realm +import timber.log.Timber + +internal interface EventRelationsAggregationTask : Task { + + data class Params( + val events: List>, + val userId: String + ) +} + +/** + * Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base. + */ +internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarchy) : EventRelationsAggregationTask { + + override fun execute(params: EventRelationsAggregationTask.Params): Try { + return monarchy.tryTransactionAsync { realm -> + update(realm, params.events, params.userId) + } + } + + fun update(realm: Realm, events: List>?, userId: String) { + events?.forEach { pair -> + val roomId = pair.first.roomId ?: return@forEach + val event = pair.first + val sendState = pair.second + val isLocalEcho = sendState == SendState.UNSENT + when (event.type) { + EventType.REACTION -> { + //we got a reaction!! + Timber.v("###REACTION in room $roomId") + handleReaction(event, roomId, realm, userId, isLocalEcho) + } + EventType.MESSAGE -> { + if (event.unsignedData?.relations?.annotations != null) { + Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") + handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) + } else { + val content: MessageContent? = event.content.toModel() + if (content?.relatesTo?.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + //A replace! + handleReplace(realm, event, content, roomId, isLocalEcho) + } + } + + } + EventType.REDACTION -> { + val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } + ?: return + when (eventToPrune.type) { + EventType.MESSAGE -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") + val unsignedData = EventMapper.map(eventToPrune).unsignedData + ?: UnsignedData(null, null) + + //was this event a m.replace + val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() + if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { + handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) + } + + } + EventType.REACTION -> { + handleReactionRedact(eventToPrune, realm, userId) + } + } + } + } + } + } + + private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) { + val eventId = event.eventId ?: return + val targetEventId = content.relatesTo?.eventId ?: return + val newContent = content.newContent ?: return + //ok, this is a replace + var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() + if (existing == null) { + Timber.v("###REPLACE creating no relation summary for ${targetEventId}") + existing = EventAnnotationsSummaryEntity.create(realm, targetEventId) + existing.roomId = roomId + } + + //we have it + val existingSummary = existing.editSummary + if (existingSummary == null) { + Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one (localEcho:$isLocalEcho)") + //create the edit summary + val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) + editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() + editSummary.aggregatedContent = ContentMapper.map(newContent) + if (isLocalEcho) { + editSummary.sourceLocalEchoEvents.add(eventId) + } else { + editSummary.sourceEvents.add(eventId) + } + + existing.editSummary = editSummary + } else { + if (existingSummary.sourceEvents.contains(eventId)) { + //ignore this event, we already know it (??) + Timber.v("###REPLACE ignoring event for summary, it's known ${eventId}") + return + } + val txId = event.unsignedData?.transactionId + //is it a remote echo? + if (!isLocalEcho && existingSummary.sourceLocalEchoEvents.contains(txId)) { + //ok it has already been managed + Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") + existingSummary.sourceLocalEchoEvents.remove(txId) + existingSummary.sourceEvents.add(event.eventId) + } else if (event.originServerTs ?: 0 > existingSummary.lastEditTs) { + Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") + existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() + existingSummary.aggregatedContent = ContentMapper.map(newContent) + existingSummary.sourceEvents.add(eventId) + } else { + //ignore this event for the summary + Timber.v("###REPLACE ignoring event for summary, it's to old ${eventId}") + } + } + + } + + private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { + aggregation.chunk?.forEach { + if (it.type == EventType.REACTION) { + val eventId = event.eventId ?: "" + val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + if (existing == null) { + val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) + eventSummary.roomId = roomId + val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = it.key + sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? + sum.count = it.count + eventSummary.reactionsSummary.add(sum) + } else { + //TODO how to handle that + } + } + } + } + + fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) { + event.content.toModel()?.let { content -> + //rel_type must be m.annotation + if (RelationType.ANNOTATION == content.relatesTo?.type) { + val reaction = content.relatesTo.key + val eventId = content.relatesTo.eventId + val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId } + + var sum = eventSummary.reactionsSummary.find { it.key == reaction } + val txId = event.unsignedData?.transactionId + if (isLocalEcho && txId.isNullOrBlank()) { + Timber.w("Received a local echo with no transaction ID") + } + if (sum == null) { + sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = reaction + sum.firstTimestamp = event.originServerTs ?: 0 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + sum.count = 1 + } else { + Timber.v("Adding synced reaction $reaction") + sum.count = 1 + sum.sourceEvents.add(event.eventId) + } + sum.addedByMe = sum.addedByMe || (userId == event.sender) + eventSummary.reactionsSummary.add(sum) + } else { + //is this a known event (is possible? pagination?) + if (!sum.sourceEvents.contains(eventId)) { + + //check if it's not the sync of a local echo + if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) { + //ok it has already been counted, just sync the list, do not touch count + Timber.v("Ignoring synced of local echo for reaction $reaction") + sum.sourceLocalEcho.remove(txId) + sum.sourceEvents.add(event.eventId) + } else { + sum.count += 1 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + } else { + Timber.v("Adding synced reaction $reaction") + sum.sourceEvents.add(event.eventId) + } + + sum.addedByMe = sum.addedByMe || (userId == event.sender) + } + + } + } + + } + } + } + + /** + * Called when an event is deleted + */ + fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) { + Timber.d("Handle redaction of m.replace") + val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst() + if (eventSummary == null) { + Timber.w("Redaction of a replace targeting an unknown event $relatedEventId") + return + } + val sourceEvents = eventSummary.editSummary?.sourceEvents + val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId) + if (sourceToDiscard == null) { + Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard") + return + } + //Need to remove this event from the redaction list and compute new aggregation state + sourceEvents.removeAt(sourceToDiscard) + val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull() + if (previousEdit == null) { + //revert to original + eventSummary.editSummary?.deleteFromRealm() + } else { + //I have the last event + ContentMapper.map(previousEdit.content)?.toModel()?.newContent?.let { newContent -> + eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs + ?: System.currentTimeMillis() + eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent) + } ?: run { + Timber.e("Failed to udate edited summary") + //TODO how to reccover that + } + + } + } + + fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) { + Timber.d("REDACTION of reaction ${eventToPrune.eventId}") + //delete a reaction, need to update the annotation summary if any + val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() + ?: return + val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return + + val reactionkey = reactionContent.relatesTo.key + Timber.d("REMOVE reaction for key $reactionkey") + val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst() + if (summary != null) { + summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionkey) + .findFirst()?.let { summary -> + Timber.d("Find summary for key with ${summary.sourceEvents.size} known reactions (count:${summary.count})") + Timber.d("Known reactions ${summary.sourceEvents.joinToString(",")}") + if (summary.sourceEvents.contains(eventToPrune.eventId)) { + Timber.d("REMOVE reaction for key $reactionkey") + summary.sourceEvents.remove(eventToPrune.eventId) + Timber.d("Known reactions after ${summary.sourceEvents.joinToString(",")}") + summary.count = summary.count - 1 + if (eventToPrune.sender == userId) { + //Was it a redact on my reaction? + summary.addedByMe = false + } + if (summary.count == 0) { + //delete! + summary.deleteFromRealm() + } + } else { + Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known") + } + } + } else { + Timber.e("## Cannot find summary for key $reactionkey") + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index 64802553..0b29064c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -15,16 +15,14 @@ */ package im.vector.matrix.android.internal.session.room +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.api.session.events.model.* -import im.vector.matrix.android.api.session.room.model.relation.ReactionContent -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.internal.database.mapper.ContentMapper -import im.vector.matrix.android.internal.database.mapper.EventMapper -import im.vector.matrix.android.internal.database.model.* -import im.vector.matrix.android.internal.database.query.create +import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where -import io.realm.Realm +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith import timber.log.Timber /** @@ -32,198 +30,34 @@ import timber.log.Timber * For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity. * The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display. */ -internal class EventRelationsAggregationUpdater(private val credentials: Credentials) { +internal class EventRelationsAggregationUpdater(monarchy: Monarchy, + private val credentials: Credentials, + private val task: EventRelationsAggregationTask, + private val taskExecutor: TaskExecutor) : + RealmLiveEntityObserver(monarchy) { - fun update(realm: Realm, roomId: String, events: List?) { - events?.forEach { event -> - when (event.type) { - EventType.REACTION -> { - //we got a reaction!! - Timber.v("###REACTION in room $roomId") - handleReaction(event, roomId, realm) - } - EventType.MESSAGE -> { - if (event.unsignedData?.relations?.annotations != null) { - Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") - handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) - } else { - val content: MessageContent? = event.content.toModel() - if (content?.relatesTo?.type == RelationType.REPLACE) { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - //A replace! - handleReplace(event, content, roomId, realm) - } - } - } - } - } + override val query = Monarchy.Query { + EventEntity.where(it) + //mmm why is this query not working? +// EventEntity.byTypes(it, listOf( +// EventType.REDACTION, EventType.MESSAGE, EventType.REDACTION) +// ) } - private fun handleReplace(event: Event, content: MessageContent, roomId: String, realm: Realm) { - val eventId = event.eventId ?: return - val targetEventId = content.relatesTo?.eventId ?: return - val newContent = content.newContent ?: return - //ok, this is a replace - var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() - if (existing == null) { - Timber.v("###REPLACE creating no relation summary for ${targetEventId}") - existing = EventAnnotationsSummaryEntity.create(realm, targetEventId) - existing.roomId = roomId - } + override fun processChanges(inserted: List, updated: List, deleted: List) { + Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions") + val inserted = inserted + .mapNotNull { it.asDomain() to it.sendState } - //we have it - val existingSummary = existing.editSummary - if (existingSummary == null) { - Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one") - //create the edit summary - val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) - editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() - editSummary.aggregatedContent = ContentMapper.map(newContent) - editSummary.sourceEvents.add(eventId) + val params = EventRelationsAggregationTask.Params( + inserted, + credentials.userId + ) - existing.editSummary = editSummary - } else { - if (existingSummary.sourceEvents.contains(eventId)) { - //ignore this event, we already know it (??) - Timber.v("###REPLACE ignoring event for summary, it's known ${eventId}") - return - } - //This message has already been edited - if (event.originServerTs ?: 0 > existingSummary.lastEditTs ?: 0) { - Timber.v("###REPLACE Computing aggregated edit summary") - existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() - existingSummary.aggregatedContent = ContentMapper.map(newContent) - existingSummary.sourceEvents.add(eventId) - } else { - //ignore this event for the summary - Timber.v("###REPLACE ignoring event for summary, it's to old ${eventId}") - } - } + task.configureWith(params) + .executeBy(taskExecutor) } - private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { - aggregation.chunk?.forEach { - if (it.type == EventType.REACTION) { - val eventId = event.eventId ?: "" - val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() - if (existing == null) { - val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) - eventSummary.roomId = roomId - val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) - sum.key = it.key - sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? - sum.count = it.count - eventSummary.reactionsSummary.add(sum) - } else { - //TODO how to handle that - } - } - } - } +} - fun handleReaction(event: Event, roomId: String, realm: Realm) { - event.content.toModel()?.let { content -> - //rel_type must be m.annotation - if (RelationType.ANNOTATION == content.relatesTo?.type) { - val reaction = content.relatesTo.key - val eventId = content.relatesTo.eventId - val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() - ?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId } - - var sum = eventSummary.reactionsSummary.find { it.key == reaction } - if (sum == null) { - sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) - sum.key = reaction - sum.firstTimestamp = event.originServerTs ?: 0 - sum.count = 1 - sum.sourceEvents.add(event.eventId) - sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender) - eventSummary.reactionsSummary.add(sum) - } else { - //is this a known event (is possible? pagination?) - if (!sum.sourceEvents.contains(eventId)) { - sum.count += 1 - sum.sourceEvents.add(event.eventId) - sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender) - } - } - - } - } - } - - /** - * Called when an event is deleted - */ - fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) { - Timber.d("Handle redaction of m.replace") - val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst() - if (eventSummary == null) { - Timber.w("Redaction of a replace targeting an unknown event $relatedEventId") - return - } - val sourceEvents = eventSummary.editSummary?.sourceEvents - val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId) - if (sourceToDiscard == null) { - Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard") - return - } - //Need to remove this event from the redaction list and compute new aggregation state - sourceEvents.removeAt(sourceToDiscard) - val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull() - if (previousEdit == null) { - //revert to original - eventSummary.editSummary?.deleteFromRealm() - } else { - //I have the last event - ContentMapper.map(previousEdit.content)?.toModel()?.newContent?.let { newContent -> - eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs - ?: System.currentTimeMillis() - eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent) - } ?: run { - Timber.e("Failed to udate edited summary") - //TODO how to reccover that - } - - } - } - - fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) { - Timber.d("REDACTION of reaction ${eventToPrune.eventId}") - //delete a reaction, need to update the annotation summary if any - val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() - ?: return - val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return - - val reactionkey = reactionContent.relatesTo.key - Timber.d("REMOVE reaction for key $reactionkey") - val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst() - if (summary != null) { - summary.reactionsSummary.where() - .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionkey) - .findFirst()?.let { summary -> - Timber.d("Find summary for key with ${summary.sourceEvents.size} known reactions (count:${summary.count})") - Timber.d("Known reactions ${summary.sourceEvents.joinToString(",")}") - if (summary.sourceEvents.contains(eventToPrune.eventId)) { - Timber.d("REMOVE reaction for key $reactionkey") - summary.sourceEvents.remove(eventToPrune.eventId) - Timber.d("Known reactions after ${summary.sourceEvents.joinToString(",")}") - summary.count = summary.count - 1 - if (eventToPrune.sender == userId) { - //Was it a redact on my reaction? - summary.addedByMe = false - } - if (summary.count == 0) { - //delete! - summary.deleteFromRealm() - } - } else { - Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known") - } - } - } else { - Timber.e("## Cannot find summary for key $reactionkey") - } - } -} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 7013b99d..7f47f576 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -17,10 +17,6 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.internal.session.DefaultSession -import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask -import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask -import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask @@ -35,6 +31,10 @@ import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTas import im.vector.matrix.android.internal.session.room.prune.PruneEventTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask +import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask +import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask @@ -57,7 +57,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - TokenChunkEventPersistor(get(), get()) + TokenChunkEventPersistor(get()) } scope(DefaultSession.SCOPE) { @@ -109,7 +109,11 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - DefaultPruneEventTask(get(), get()) as PruneEventTask + DefaultPruneEventTask(get()) as PruneEventTask + } + + scope(DefaultSession.SCOPE) { + DefaultEventRelationsAggregationTask(get()) as EventRelationsAggregationTask } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index cb7a3818..4d3bc6fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -25,8 +25,12 @@ import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber - +/** + * Listens to the database for the insertion of any redaction event. + * As it will actually delete the content, it should be called last in the list of listener. + */ internal class EventsPruner(monarchy: Monarchy, private val credentials: Credentials, private val pruneEventTask: PruneEventTask, @@ -36,6 +40,7 @@ internal class EventsPruner(monarchy: Monarchy, override val query = Monarchy.Query { EventEntity.where(it, type = EventType.REDACTION) } override fun processChanges(inserted: List, updated: List, deleted: List) { + Timber.v("Event pruner called with ${inserted.size} insertions") val redactionEvents = inserted .mapNotNull { it.asDomain() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt index 44c62a09..a4025978 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt @@ -17,14 +17,15 @@ package im.vector.matrix.android.internal.session.room.prune import arrow.core.Try import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.events.model.* -import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.UnsignedData +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.MoshiProvider -import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.tryTransactionSync import io.realm.Realm @@ -41,8 +42,7 @@ internal interface PruneEventTask : Task { } internal class DefaultPruneEventTask( - private val monarchy: Monarchy, - private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) : PruneEventTask { + private val monarchy: Monarchy) : PruneEventTask { override fun execute(params: PruneEventTask.Params): Try { return monarchy.tryTransactionSync { realm -> @@ -57,6 +57,12 @@ internal class DefaultPruneEventTask( return } + val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId + ?: "").findFirst() + ?: return + val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT + Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho") + val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() ?: return @@ -72,19 +78,19 @@ internal class DefaultPruneEventTask( ?: UnsignedData(null, null) //was this event a m.replace - val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() - if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { - eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) - } +// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() +// if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { +// eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) +// } val modified = unsignedData.copy(redactedEvent = redactionEvent) eventToPrune.content = ContentMapper.map(emptyMap()) eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) } - EventType.REACTION -> { - eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) - } +// EventType.REACTION -> { +// eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) +// } } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 0c2addd5..6d6e4763 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -15,12 +15,12 @@ */ package im.vector.matrix.android.internal.session.room.relation -import androidx.work.* +import androidx.work.OneTimeWorkRequest import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -30,19 +30,14 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.RedactEventWorker import im.vector.matrix.android.internal.session.room.send.SendEventWorker +import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.tryTransactionAsync -import java.util.concurrent.TimeUnit +import timber.log.Timber -private const val REACTION_WORK = "REACTION_WORK" -private const val BACKOFF_DELAY = 10_000L - -private val WORK_CONSTRAINTS = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() internal class DefaultRelationService(private val roomId: String, private val eventFactory: LocalEchoEventFactory, @@ -55,28 +50,22 @@ internal class DefaultRelationService(private val roomId: String, override fun sendReaction(reaction: String, targetEventId: String): Cancelable { val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) -// .also { -// //saveLocalEcho(it) -// } + .also { + saveLocalEcho(it) + } val sendRelationWork = createSendRelationWork(event) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, sendRelationWork) - .enqueue() + TimelineSendEventWorkCommon.postWork(roomId, sendRelationWork) return CancelableWork(sendRelationWork.id) } private fun createSendRelationWork(event: Event): OneTimeWorkRequest { - //TODO use the new API to send relation (for now use regular send) val sendContentWorkerParams = SendEventWorker.Params( roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(sendWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return TimelineSendEventWorkCommon.createWork(sendWorkData) + } override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { @@ -91,11 +80,19 @@ internal class DefaultRelationService(private val roomId: String, .enableRetry() .dispatchTo(object : MatrixCallback { override fun onSuccess(data: FindReactionEventForUndoTask.Result) { + if (data.redactEventId == null) { + Timber.w("Cannot find reaction to undo (not yet synced?)") + //TODO? + } data.redactEventId?.let { toRedact -> - val redactWork = createRedactEventWork(toRedact, null) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork) - .enqueue() + + val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also { + saveLocalEcho(it) + } + val redactWork = createRedactEventWork(redactEvent, toRedact, null) + + TimelineSendEventWorkCommon.postWork(roomId, redactWork) + } } }) @@ -119,10 +116,11 @@ internal class DefaultRelationService(private val roomId: String, override fun onSuccess(data: UpdateQuickReactionTask.Result) { data.reactionToAdd?.also { sendReaction(it, targetEventId) } data.reactionToRedact.forEach { - val redactWork = createRedactEventWork(it, null) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork) - .enqueue() + val redactEvent = eventFactory.createRedactEvent(roomId, it, null).also { + saveLocalEcho(it) + } + val redactWork = createRedactEventWork(redactEvent, it, null) + TimelineSendEventWorkCommon.postWork(roomId, redactWork) } } }) @@ -133,48 +131,26 @@ internal class DefaultRelationService(private val roomId: String, return "${roomId}_$identifier" } -// private fun saveLocalEcho(event: Event) { -// monarchy.tryTransactionAsync { realm -> -// val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() -// ?: return@tryTransactionAsync -// val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId) -// ?: return@tryTransactionAsync -// -// roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) -// } -// } - //TODO duplicate with send service? - private fun createRedactEventWork(eventId: String, reason: String?): OneTimeWorkRequest { + private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest { - //TODO create local echo of m.room.redaction event? - - val sendContentWorkerParams = RedactEventWorker.Params( + val sendContentWorkerParams = RedactEventWorker.Params(localEvent.eventId!!, roomId, eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - - return OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(redactWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return TimelineSendEventWorkCommon.createWork(redactWorkData) } override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable { - val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText) + val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText).also { + saveLocalEcho(it) + } val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) //TODO use relation API? - val workRequest = OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(sendWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest) - .enqueue() + val workRequest = TimelineSendEventWorkCommon.createWork(sendWorkData) + TimelineSendEventWorkCommon.postWork(roomId, workRequest) return CancelableWork(workRequest.id) } @@ -186,18 +162,19 @@ internal class DefaultRelationService(private val roomId: String, } ?: return null val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - val workRequest = OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(sendWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest) - .enqueue() + + val workRequest = TimelineSendEventWorkCommon.createWork(sendWorkData) + TimelineSendEventWorkCommon.postWork(roomId, workRequest) return CancelableWork(workRequest.id) } + /** + * Saves the event in database as a local echo. + * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room. + * The sendingTimelineEvents is checked on new sync and will remove the local echo if an event with + * the same transaction id is received (in unsigned data) + */ private fun saveLocalEcho(event: Event) { monarchy.tryTransactionAsync { realm -> val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt index 8fac4ed2..41184aaf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt @@ -70,7 +70,11 @@ class SendRelationWorker(context: Context, params: WorkerParameters) return result.fold({ when (it) { is Failure.NetworkConnection -> Result.retry() - else -> Result.failure() + else -> { + //TODO mark as failed to send? + //always return success, or the chain will be stuck for ever! + Result.success() + } } }, { Result.success() }) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 6b16ffaf..d5b8a1c7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -24,18 +24,12 @@ import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.addTo -import im.vector.matrix.android.internal.database.helper.addSendingEvent -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.UploadContentWorker +import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.WorkerParamsFactory -import im.vector.matrix.android.internal.util.tryTransactionAsync import java.util.concurrent.TimeUnit -private const val SEND_WORK = "SEND_WORK" private const val UPLOAD_WORK = "UPLOAD_WORK" private const val BACKOFF_DELAY = 10_000L @@ -54,9 +48,7 @@ internal class DefaultSendService(private val roomId: String, saveLocalEcho(it) } val sendWork = createSendEventWork(event) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork) - .enqueue() + TimelineSendEventWorkCommon.postWork(roomId, sendWork) return CancelableWork(sendWork.id) } @@ -65,9 +57,7 @@ internal class DefaultSendService(private val roomId: String, saveLocalEcho(it) } val sendWork = createSendEventWork(event) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork) - .enqueue() + TimelineSendEventWorkCommon.postWork(roomId, sendWork) return CancelableWork(sendWork.id) } @@ -80,12 +70,9 @@ internal class DefaultSendService(private val roomId: String, } override fun redactEvent(event: Event, reason: String?): Cancelable { - //TODO manage local echo ? //TODO manage media/attachements? val redactWork = createRedactEventWork(event, reason) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, redactWork) - .enqueue() + TimelineSendEventWorkCommon.postWork(roomId, redactWork) return CancelableWork(redactWork.id) } @@ -106,14 +93,7 @@ internal class DefaultSendService(private val roomId: String, } private fun saveLocalEcho(event: Event) { - monarchy.tryTransactionAsync { realm -> - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - ?: return@tryTransactionAsync - val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId) - ?: return@tryTransactionAsync - - roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) - } + eventFactory.saveLocalEcho(monarchy, event) } private fun buildWorkIdentifier(identifier: String): String { @@ -124,26 +104,20 @@ internal class DefaultSendService(private val roomId: String, val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(sendWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return TimelineSendEventWorkCommon.createWork(sendWorkData) } private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { - //TODO create local echo of m.room.redaction event? + val redactEvent = eventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { + saveLocalEcho(it) + } - val sendContentWorkerParams = RedactEventWorker.Params( - roomId, event.eventId!!, reason) + val sendContentWorkerParams = RedactEventWorker.Params(redactEvent.eventId!!, + roomId, event.eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(redactWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return TimelineSendEventWorkCommon.createWork(redactWorkData) } private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index a810baac..582862d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -18,21 +18,37 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever import android.text.TextUtils +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.* +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.internal.database.helper.addSendingEvent +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.util.StringProvider +import im.vector.matrix.android.internal.util.tryTransactionAsync import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer +/** + * Creates local echo of events for room events. + * A local echo is an event that is persisted even if not yet sent to the server, + * in an optimistic way (as if the server as responded immediately). Local echo are using a local id, + * (the transaction ID), this id is used when receiving an event from a sync to check if this event + * is matching an existing local echo. + * + * The transactionID is used as loc + */ internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) { fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { @@ -41,7 +57,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva val document = parser.parse(text) val renderer = HtmlRenderer.builder().build() val htmlText = renderer.render(document) - if (!TextUtils.equals(text, htmlText)) { + if (isFormattedTextPertinent(text, htmlText)) { //FIX that return createFormattedTextEvent(roomId, text, htmlText) } } @@ -49,6 +65,9 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva return createEvent(roomId, content) } + private fun isFormattedTextPertinent(text: String, htmlText: String?) = + text != htmlText && htmlText != "

$text

\n" + fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event { val content = MessageTextContent( type = MessageType.MSGTYPE_TEXT, @@ -71,7 +90,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva val document = parser.parse(newBodyText) val renderer = HtmlRenderer.builder().build() val htmlText = renderer.render(document) - if (!TextUtils.equals(newBodyText, htmlText)) { + if (isFormattedTextPertinent(newBodyText, htmlText)) { newContent = MessageTextContent( type = MessageType.MSGTYPE_TEXT, format = MessageType.FORMAT_MATRIX_HTML, @@ -107,14 +126,16 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva reaction ) ) + val localId = dummyEventId(roomId) return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), sender = credentials.userId, - eventId = dummyEventId(roomId), + eventId = localId, type = EventType.REACTION, - content = content.toContent() - ) + content = content.toContent(), + unsignedData = UnsignedData(age = null, transactionId = localId)) + } @@ -196,13 +217,15 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva } private fun createEvent(roomId: String, content: Any? = null): Event { + val localID = dummyEventId(roomId) return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), sender = credentials.userId, - eventId = dummyEventId(roomId), + eventId = localID, type = EventType.MESSAGE, - content = content.toContent() + content = content.toContent(), + unsignedData = UnsignedData(age = null, transactionId = localID) ) } @@ -211,7 +234,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva } private fun dummyEventId(roomId: String): String { - return roomId + "-" + dummyOriginServerTs() + return "m.${txNCounter++}" } fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? { @@ -285,4 +308,49 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva } } + + /* + * { + "content": { + "reason": "Spamming" + }, + "event_id": "$143273582443PhrSn:domain.com", + "origin_server_ts": 1432735824653, + "redacts": "$fukweghifu23:localhost", + "room_id": "!jEsUZKDJdhlrceRyVU:domain.com", + "sender": "@example:domain.com", + "type": "m.room.redaction", + "unsigned": { + "age": 1234 + } + } + */ + fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event { + val localID = dummyEventId(roomId) + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + sender = credentials.userId, + eventId = localID, + type = EventType.REDACTION, + redacts = eventId, + content = reason?.let { mapOf("reason" to it).toContent() }, + unsignedData = UnsignedData(age = null, transactionId = localID) + ) + } + + fun saveLocalEcho(monarchy: Monarchy, event: Event) { + monarchy.tryTransactionAsync { realm -> + val roomEntity = RoomEntity.where(realm, roomId = event.roomId!!).findFirst() + ?: return@tryTransactionAsync + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = event.roomId) + ?: return@tryTransactionAsync + + roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) + } + } + + companion object { + var txNCounter = System.currentTimeMillis() + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt index e0bc740e..5056a93b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt @@ -25,13 +25,13 @@ import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.util.WorkerParamsFactory import org.koin.standalone.inject -import java.util.* internal class RedactEventWorker(context: Context, params: WorkerParameters) : Worker(context, params), MatrixKoinComponent { @JsonClass(generateAdapter = true) internal data class Params( + val txID: String, val roomId: String, val eventId: String, val reason: String? @@ -40,26 +40,26 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) private val roomAPI by inject() override fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) + val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() - if (params.eventId == null) { - return Result.failure() - } - val txID = UUID.randomUUID().toString() - + val eventId = params.eventId val result = executeRequest { apiCall = roomAPI.redactEvent( - txID, + params.txID, params.roomId, - params.eventId, + eventId, if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) ) } return result.fold({ when (it) { is Failure.NetworkConnection -> Result.retry() - else -> Result.failure() + else -> { + //TODO mark as failed to send? + //always return success, or the chain will be stuck for ever! + Result.success() + } } }, { Result.success() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 73330770..f9a0478b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -61,7 +61,11 @@ internal class SendEventWorker(context: Context, params: WorkerParameters) return result.fold({ when (it) { is Failure.NetworkConnection -> Result.retry() - else -> Result.failure() + else -> { + //TODO mark as failed to send? + //always return success, or the chain will be stuck for ever! + Result.success() + } } }, { Result.success() }) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt new file mode 100644 index 00000000..8205f2cc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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 + * + * 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. + */ +package im.vector.matrix.android.internal.session.room.timeline + +import androidx.work.* +import java.util.concurrent.TimeUnit + + +private const val SEND_WORK = "SEND_WORK" +private const val BACKOFF_DELAY = 10_000L + +private val WORK_CONSTRAINTS = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + +/** + * Helper class for sending event related works. + * All send event from a room are using the same workchain, in order to ensure order. + * WorkRequest must always return success (even if server error, in this case marking the event as failed to send) + * , if not the chain will be doomed in failed state. + * + */ +internal object TimelineSendEventWorkCommon { + + fun postWork(roomId: String, workRequest: OneTimeWorkRequest) { + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, workRequest) + .enqueue() + + } + + inline fun createWork(data: Data): OneTimeWorkRequest { + return OneTimeWorkRequestBuilder() + .setConstraints(WORK_CONSTRAINTS) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun buildWorkIdentifier(roomId: String): String { + return "${roomId}_$SEND_WORK" + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index ec85c558..9c40b4aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -18,20 +18,13 @@ package im.vector.matrix.android.internal.session.room.timeline import arrow.core.Try import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.internal.database.helper.addAll -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.helper.addStateEvents -import im.vector.matrix.android.internal.database.helper.deleteOnCascade -import im.vector.matrix.android.internal.database.helper.isUnlinked -import im.vector.matrix.android.internal.database.helper.merge -import im.vector.matrix.android.internal.database.mapper.EventMapper +import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater import im.vector.matrix.android.internal.util.tryTransactionSync import io.realm.kotlin.createObject import timber.log.Timber @@ -39,8 +32,7 @@ import timber.log.Timber /** * Insert Chunk in DB, and eventually merge with existing chunk event */ -internal class TokenChunkEventPersistor(private val monarchy: Monarchy, - private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) { +internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { /** *
@@ -119,7 +111,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
                     Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
 
                     val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                                     ?: realm.createObject(roomId)
+                            ?: realm.createObject(roomId)
 
                     val nextToken: String?
                     val prevToken: String?
@@ -142,7 +134,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
                     } else {
                         nextChunk?.apply { this.prevToken = prevToken }
                     }
-                                       ?: ChunkEntity.create(realm, prevToken, nextToken)
+                            ?: ChunkEntity.create(realm, prevToken, nextToken)
 
                     if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
                         Timber.v("Reach end of $roomId")
@@ -151,8 +143,6 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
                         Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
                         currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
 
-                        //Event
-                        eventRelationsAggregationUpdater.update(realm,roomId,receivedChunk.events.toList())
                         // Then we merge chunks if needed
                         if (currentChunk != prevChunk && prevChunk != null) {
                             currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
index eaab9846..018cc518 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
@@ -31,14 +31,9 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
 import im.vector.matrix.android.internal.database.query.find
 import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
 import im.vector.matrix.android.internal.database.query.where
-import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
 import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
 import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
-import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
-import im.vector.matrix.android.internal.session.sync.model.RoomSync
-import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
-import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
-import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
+import im.vector.matrix.android.internal.session.sync.model.*
 import io.realm.Realm
 import io.realm.kotlin.createObject
 import timber.log.Timber
@@ -46,8 +41,7 @@ import timber.log.Timber
 internal class RoomSyncHandler(private val monarchy: Monarchy,
                                private val readReceiptHandler: ReadReceiptHandler,
                                private val roomSummaryUpdater: RoomSummaryUpdater,
-                               private val roomTagHandler: RoomTagHandler,
-                               private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
+                               private val roomTagHandler: RoomTagHandler) {
 
     sealed class HandlingStrategy {
         data class JOINED(val data: Map) : HandlingStrategy()
@@ -67,9 +61,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
 
     private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) {
         val rooms = when (handlingStrategy) {
-            is HandlingStrategy.JOINED  -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) }
+            is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) }
             is HandlingStrategy.INVITED -> handlingStrategy.data.map { handleInvitedRoom(realm, it.key, it.value) }
-            is HandlingStrategy.LEFT    -> handlingStrategy.data.map { handleLeftRoom(realm, it.key, it.value) }
+            is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(realm, it.key, it.value) }
         }
         realm.insertOrUpdate(rooms)
     }
@@ -81,7 +75,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
         Timber.v("Handle join sync for room $roomId")
 
         val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                         ?: realm.createObject(roomId)
+                ?: realm.createObject(roomId)
 
         if (roomEntity.membership == Membership.INVITE) {
             roomEntity.chunks.deleteAllFromRealm()
@@ -116,11 +110,13 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
             transactionIds.forEach {
                 val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
                 if (sendingEventEntity != null) {
+                    Timber.v("Remove local echo for tx:$it")
                     roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
+                } else {
+                    Timber.v("Can't find corresponding local echo for tx:$it")
                 }
             }
         }
-        eventRelationsAggregationUpdater.update(realm, roomId, roomSync.timeline?.events)
         roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
 
         if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
@@ -139,7 +135,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
                                   InvitedRoomSync): RoomEntity {
         Timber.v("Handle invited sync for room $roomId")
         val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                         ?: realm.createObject(roomId)
+                ?: realm.createObject(roomId)
         roomEntity.membership = Membership.INVITE
         if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
             val chunkEntity = handleTimelineEvents(realm, roomId, roomSync.inviteState.events)
@@ -153,7 +149,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
                                roomId: String,
                                roomSync: RoomSync): RoomEntity {
         val roomEntity = RoomEntity.where(realm, roomId).findFirst()
-                         ?: realm.createObject(roomId)
+                ?: realm.createObject(roomId)
 
         roomEntity.membership = Membership.LEAVE
         roomEntity.chunks.deleteAllFromRealm()
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt
index d6014061..d0f60405 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt
@@ -40,7 +40,7 @@ internal class SyncModule {
         }
 
         scope(DefaultSession.SCOPE) {
-            RoomSyncHandler(get(), get(), get(), get(), get())
+            RoomSyncHandler(get(), get(), get(), get())
         }
 
         scope(DefaultSession.SCOPE) {
diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 05fda074..4509b19e 100644
--- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -90,7 +90,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
                 avatarUrl = avatarUrl,
                 memberName = formattedMemberName,
                 showInformation = showInformation,
-                orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) },
+                orderedReactionList = event.annotations?.reactionsSummary?.map {
+                    ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
+                },
                 hasBeenEdited = hasBeenEdited
         )
 
diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt
index d560424d..0ce01a88 100644
--- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt
+++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt
@@ -112,11 +112,12 @@ abstract class AbsMessageItem : BaseEventItem() {
                 (holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
                     reactionButton.isVisible = true
                     reactionButton.reactedListener = reactionClickListener
-                    reactionButton.setTag(R.id.messageBottomInfo, reaction.first)
+                    reactionButton.setTag(R.id.messageBottomInfo, reaction.key)
                     idToRefInFlow.add(reactionButton.id)
-                    reactionButton.reactionString = reaction.first
-                    reactionButton.reactionCount = reaction.second
-                    reactionButton.setChecked(reaction.third)
+                    reactionButton.reactionString = reaction.key
+                    reactionButton.reactionCount = reaction.count
+                    reactionButton.setChecked(reaction.addedByMe)
+                    reactionButton.isEnabled = reaction.synced
                 }
             }
             // Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt
index 5cdd1f9c..e7f4aa82 100644
--- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt
+++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt
@@ -16,9 +16,8 @@
 
 package im.vector.riotredesign.features.home.room.detail.timeline.item
 
-import im.vector.matrix.android.api.session.room.send.SendState
-
 import android.os.Parcelable
+import im.vector.matrix.android.api.session.room.send.SendState
 import kotlinx.android.parcel.Parcelize
 
 @Parcelize
@@ -31,6 +30,15 @@ data class MessageInformationData(
         val memberName: CharSequence? = null,
         val showInformation: Boolean = true,
         /*List of reactions (emoji,count,isSelected)*/
-        var orderedReactionList: List>? = null,
+        var orderedReactionList: List? = null,
         var hasBeenEdited: Boolean = false
-) : Parcelable
\ No newline at end of file
+) : Parcelable
+
+
+@Parcelize
+data class ReactionInfoData(
+        val key: String,
+        val count: Int,
+        val addedByMe: Boolean,
+        val synced: Boolean
+) : Parcelable

From 424fd1347d4c916581e13954d8e62f18af6836b1 Mon Sep 17 00:00:00 2001
From: Valere 
Date: Mon, 3 Jun 2019 18:23:40 +0200
Subject: [PATCH 16/17] Code review

---
 .../database/query/EventEntityQueries.kt      | 28 +----------
 .../room/EventRelationsAggregationTask.kt     | 46 +++++++++----------
 .../room/send/LocalEchoEventFactory.kt        | 21 ++++-----
 3 files changed, 34 insertions(+), 61 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt
index c366ff75..719407ca 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt
@@ -16,7 +16,6 @@
 
 package im.vector.matrix.android.internal.database.query
 
-import im.vector.matrix.android.api.session.room.send.SendState
 import im.vector.matrix.android.internal.database.model.ChunkEntity
 import im.vector.matrix.android.internal.database.model.EventEntity
 import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
@@ -43,35 +42,12 @@ internal fun EventEntity.Companion.where(realm: Realm,
         query.equalTo(EventEntityFields.TYPE, type)
     }
     return when (linkFilterMode) {
-        LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false)
+        LINKED_ONLY   -> query.equalTo(EventEntityFields.IS_UNLINKED, false)
         UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true)
-        BOTH -> query
+        BOTH          -> query
     }
 }
 
-//internal fun EventEntity.Companion.unsent(realm: Realm,
-//                                          roomId: String? = null): RealmQuery {
-//    val query = realm.where()
-//    if (roomId != null) {
-//        query.equalTo(EventEntityFields.ROOM_ID, roomId)
-//    }
-//    query.equalTo(EventEntityFields.SEND_STATE_STR, SendState.UNSENT.name)
-//    return query
-//}
-//
-//internal fun EventEntity.Companion.byTypes(realm: Realm,
-//                                           types: List): RealmQuery {
-//    val query = realm.where()
-//    types.forEachIndexed { index, type ->
-//        if (index == 0) {
-//            query.equalTo(EventEntityFields.TYPE, type)
-//        } else {
-//            query.or().equalTo(EventEntityFields.TYPE, type)
-//        }
-//    }
-//    return query
-//}
-
 
 internal fun EventEntity.Companion.latestEvent(realm: Realm,
                                                roomId: String,
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
index 5181bb30..4ff661e2 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
@@ -50,19 +50,19 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
         }
     }
 
-    fun update(realm: Realm, events: List>?, userId: String) {
-        events?.forEach { pair ->
+    private fun update(realm: Realm, events: List>, userId: String) {
+        events.forEach { pair ->
             val roomId = pair.first.roomId ?: return@forEach
             val event = pair.first
             val sendState = pair.second
             val isLocalEcho = sendState == SendState.UNSENT
             when (event.type) {
-                EventType.REACTION -> {
+                EventType.REACTION  -> {
                     //we got a reaction!!
                     Timber.v("###REACTION in room $roomId")
                     handleReaction(event, roomId, realm, userId, isLocalEcho)
                 }
-                EventType.MESSAGE -> {
+                EventType.MESSAGE   -> {
                     if (event.unsignedData?.relations?.annotations != null) {
                         Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
                         handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
@@ -80,7 +80,7 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
                     val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
                             ?: return
                     when (eventToPrune.type) {
-                        EventType.MESSAGE -> {
+                        EventType.MESSAGE  -> {
                             Timber.d("REDACTION for message ${eventToPrune.eventId}")
                             val unsignedData = EventMapper.map(eventToPrune).unsignedData
                                     ?: UnsignedData(null, null)
@@ -174,7 +174,7 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
         }
     }
 
-    fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) {
+    private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) {
         event.content.toModel()?.let { content ->
             //rel_type must be m.annotation
             if (RelationType.ANNOTATION == content.relatesTo?.type) {
@@ -236,7 +236,7 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
     /**
      * Called when an event is deleted
      */
-    fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
+    private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
         Timber.d("Handle redaction of m.replace")
         val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst()
         if (eventSummary == null) {
@@ -270,40 +270,40 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
     }
 
     fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
-        Timber.d("REDACTION of reaction ${eventToPrune.eventId}")
+        Timber.v("REDACTION of reaction ${eventToPrune.eventId}")
         //delete a reaction, need to update the annotation summary if any
         val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel()
                 ?: return
         val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return
 
-        val reactionkey = reactionContent.relatesTo.key
-        Timber.d("REMOVE reaction for key $reactionkey")
+        val reactionKey = reactionContent.relatesTo.key
+        Timber.v("REMOVE reaction for key $reactionKey")
         val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst()
         if (summary != null) {
             summary.reactionsSummary.where()
-                    .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionkey)
-                    .findFirst()?.let { summary ->
-                        Timber.d("Find summary for key with  ${summary.sourceEvents.size} known reactions (count:${summary.count})")
-                        Timber.d("Known reactions  ${summary.sourceEvents.joinToString(",")}")
-                        if (summary.sourceEvents.contains(eventToPrune.eventId)) {
-                            Timber.d("REMOVE reaction for key $reactionkey")
-                            summary.sourceEvents.remove(eventToPrune.eventId)
-                            Timber.d("Known reactions after  ${summary.sourceEvents.joinToString(",")}")
-                            summary.count = summary.count - 1
+                    .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionKey)
+                    .findFirst()?.let { aggregation ->
+                        Timber.v("Find summary for key with  ${aggregation.sourceEvents.size} known reactions (count:${aggregation.count})")
+                        Timber.v("Known reactions  ${aggregation.sourceEvents.joinToString(",")}")
+                        if (aggregation.sourceEvents.contains(eventToPrune.eventId)) {
+                            Timber.v("REMOVE reaction for key $reactionKey")
+                            aggregation.sourceEvents.remove(eventToPrune.eventId)
+                            Timber.v("Known reactions after  ${aggregation.sourceEvents.joinToString(",")}")
+                            aggregation.count = aggregation.count - 1
                             if (eventToPrune.sender == userId) {
                                 //Was it a redact on my reaction?
-                                summary.addedByMe = false
+                                aggregation.addedByMe = false
                             }
-                            if (summary.count == 0) {
+                            if (aggregation.count == 0) {
                                 //delete!
-                                summary.deleteFromRealm()
+                                aggregation.deleteFromRealm()
                             }
                         } else {
                             Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known")
                         }
                     }
         } else {
-            Timber.e("## Cannot find summary for key $reactionkey")
+            Timber.e("## Cannot find summary for key $reactionKey")
         }
     }
 }
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
index 582862d6..e54af1ca 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt
@@ -17,7 +17,6 @@
 package im.vector.matrix.android.internal.session.room.send
 
 import android.media.MediaMetadataRetriever
-import android.text.TextUtils
 import com.zhuinden.monarchy.Monarchy
 import im.vector.matrix.android.R
 import im.vector.matrix.android.api.auth.data.Credentials
@@ -39,6 +38,7 @@ import im.vector.matrix.android.internal.util.StringProvider
 import im.vector.matrix.android.internal.util.tryTransactionAsync
 import org.commonmark.parser.Parser
 import org.commonmark.renderer.html.HtmlRenderer
+import java.util.*
 
 /**
  * Creates local echo of events for room events.
@@ -57,7 +57,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
             val document = parser.parse(text)
             val renderer = HtmlRenderer.builder().build()
             val htmlText = renderer.render(document)
-            if (isFormattedTextPertinent(text, htmlText)) { //FIX that
+            if (isFormattedTextPertinent(text, htmlText)) { //FIXME
                 return createFormattedTextEvent(roomId, text, htmlText)
             }
         }
@@ -114,7 +114,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
             ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
             ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment)
             ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment)
-            ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment)
+            ContentAttachmentData.Type.FILE  -> createFileEvent(roomId, attachment)
         }
     }
 
@@ -234,7 +234,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
     }
 
     private fun dummyEventId(roomId: String): String {
-        return "m.${txNCounter++}"
+        return "m.${UUID.randomUUID()}"
     }
 
     fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? {
@@ -299,11 +299,11 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
                 }
                 return content.body to formattedText
             }
-            MessageType.MSGTYPE_FILE -> return stringProvider.getString(R.string.reply_to_a_file) to null
-            MessageType.MSGTYPE_AUDIO -> return stringProvider.getString(R.string.reply_to_an_audio_file) to null
-            MessageType.MSGTYPE_IMAGE -> return stringProvider.getString(R.string.reply_to_an_image) to null
-            MessageType.MSGTYPE_VIDEO -> return stringProvider.getString(R.string.reply_to_a_video) to null
-            else -> return (content?.body ?: "") to null
+            MessageType.MSGTYPE_FILE   -> return stringProvider.getString(R.string.reply_to_a_file) to null
+            MessageType.MSGTYPE_AUDIO  -> return stringProvider.getString(R.string.reply_to_an_audio_file) to null
+            MessageType.MSGTYPE_IMAGE  -> return stringProvider.getString(R.string.reply_to_an_image) to null
+            MessageType.MSGTYPE_VIDEO  -> return stringProvider.getString(R.string.reply_to_a_video) to null
+            else                       -> return (content?.body ?: "") to null
 
         }
 
@@ -350,7 +350,4 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
         }
     }
 
-    companion object {
-        var txNCounter = System.currentTimeMillis()
-    }
 }

From e3983deacce4f0cdfadf175b2b26bcc2a2998ded Mon Sep 17 00:00:00 2001
From: Benoit Marty 
Date: Tue, 4 Jun 2019 11:02:40 +0200
Subject: [PATCH 17/17] dispose uiDisposable in onDestroy()

---
 .../im/vector/riotredesign/core/platform/VectorBaseActivity.kt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt
index 3ae8f3a8..c940d70b 100644
--- a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt
@@ -93,7 +93,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
     }
 
     protected fun Disposable.disposeOnDestroy(): Disposable {
-        // TODO Ganfra: never disposed...
         uiDisposables.add(this)
         return this
     }
@@ -133,6 +132,8 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
 
         unBinder?.unbind()
         unBinder = null
+
+        uiDisposables.dispose()
     }
 
     override fun onResume() {