Design update

+ Reply 
+ Better preview in action menu
This commit is contained in:
Valere 2019-05-27 11:55:20 +02:00
parent b45cc0e63f
commit 0e06908a48
26 changed files with 350 additions and 59 deletions

View File

@ -18,8 +18,10 @@ package im.vector.matrix.android.api.session.events.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Types
import im.vector.matrix.android.internal.di.MoshiProvider
import timber.log.Timber
import java.lang.reflect.ParameterizedType
typealias Content = Map<String, @JvmSuppressWildcards Any>
@ -31,7 +33,12 @@ inline fun <reified T> Content?.toModel(): T? {
return this?.let {
val moshi = MoshiProvider.providesMoshi()
val moshiAdapter = moshi.adapter(T::class.java)
return moshiAdapter.fromJsonValue(it)
try {
return moshiAdapter.fromJsonValue(it)
} catch (e: JsonDataException) {
Timber.e(e, "Failed to parse content")
return null
}
}
}

View File

@ -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.ReactionService
import im.vector.matrix.android.api.session.room.model.annotation.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
@ -28,7 +28,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService
/**
* This interface defines methods to interact within a room.
*/
interface Room : TimelineService, SendService, ReadService, MembershipService, StateService , ReactionService{
interface Room : TimelineService, SendService, ReadService, MembershipService, StateService , RelationService{
/**
* The roomId of this room

View File

@ -7,5 +7,7 @@ import com.squareup.moshi.JsonClass
data class ReactionInfo(
@Json(name = "rel_type") override val type: String?,
@Json(name = "event_id") override val eventId: String,
val key: String
val key: String,
//always null for reaction
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null
) : RelationContent

View File

@ -3,4 +3,5 @@ package im.vector.matrix.android.api.session.room.model.annotation
interface RelationContent {
val type: String?
val eventId: String?
val inReplyTo: ReplyToContent?
}

View File

@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RelationDefaultContent(
@Json(name = "rel_type") override val type: String?,
@Json(name = "event_id") override val eventId: String?
@Json(name = "event_id") override val eventId: String?,
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null
) : RelationContent

View File

@ -15,10 +15,13 @@
*/
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?
interface ReactionService {
interface RelationService {
/**
@ -59,4 +62,7 @@ interface ReactionService {
*/
fun editTextMessage(targetEventId: String, newBodyText: String, compatibilityBodyText: String = "* $newBodyText"): Cancelable
fun replyToMessage(eventReplied: Event, replyText: String) : Cancelable?
}

View File

@ -0,0 +1,25 @@
/*
* 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.api.session.room.model.annotation
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ReplyToContent(
@Json(name = "event_id") val eventId: String
)

View File

@ -20,6 +20,7 @@ import android.content.Context
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.StringProvider
import kotlinx.coroutines.Dispatchers
import org.koin.dsl.module.module
@ -39,6 +40,9 @@ class MatrixModule(private val context: Context) {
single {
TaskExecutor(get())
}
single {
StringProvider(context.resources)
}
single {
BackgroundDetectionObserver()

View File

@ -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.ReactionService
import im.vector.matrix.android.api.session.room.model.annotation.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
@ -40,14 +40,14 @@ internal class DefaultRoom(
private val sendService: SendService,
private val stateService: StateService,
private val readService: ReadService,
private val reactionService: ReactionService,
private val relationService: RelationService,
private val roomMembersService: MembershipService
) : Room,
TimelineService by timelineService,
SendService by sendService,
StateService by stateService,
ReadService by readService,
ReactionService by reactionService,
RelationService by relationService,
MembershipService by roomMembersService {
override val roomSummary: LiveData<RoomSummary> by lazy {

View File

@ -18,10 +18,10 @@ 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.membership.DefaultMembershipService
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.DefaultReactionService
import im.vector.matrix.android.internal.session.room.annotation.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
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
@ -58,7 +58,7 @@ internal class RoomFactory(private val monarchy: Monarchy,
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor())
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask)
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
val reactionService = DefaultReactionService(roomId, eventFactory, findReactionEventForUndoTask, updateQuickReactionTask, taskExecutor)
val reactionService = DefaultRelationService(roomId, eventFactory, findReactionEventForUndoTask, updateQuickReactionTask, monarchy, taskExecutor)
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask)

View File

@ -39,6 +39,7 @@ 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
@ -73,7 +74,7 @@ class RoomModule {
}
scope(DefaultSession.SCOPE) {
LocalEchoEventFactory(get())
LocalEchoEventFactory(get(), get())
}
scope(DefaultSession.SCOPE) {
@ -109,7 +110,7 @@ class RoomModule {
}
scope(DefaultSession.SCOPE) {
DefaultPruneEventTask(get(),get()) as PruneEventTask
DefaultPruneEventTask(get(), get()) as PruneEventTask
}
}

View File

@ -16,11 +16,17 @@
package im.vector.matrix.android.internal.session.room.annotation
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.ReactionService
import im.vector.matrix.android.api.session.room.model.annotation.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
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.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.send.RedactEventWorker
import im.vector.matrix.android.internal.session.room.send.SendEventWorker
@ -28,6 +34,7 @@ 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
private const val REACTION_WORK = "REACTION_WORK"
@ -37,12 +44,13 @@ private val WORK_CONSTRAINTS = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
internal class DefaultReactionService(private val roomId: String,
internal class DefaultRelationService(private val roomId: String,
private val eventFactory: LocalEchoEventFactory,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val updateQuickReactionTask: UpdateQuickReactionTask,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor)
: ReactionService {
: RelationService {
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
@ -170,4 +178,38 @@ internal class DefaultReactionService(private val roomId: String,
return CancelableWork(workRequest.id)
}
/**
* 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)
} ?: return null
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
val workRequest = OneTimeWorkRequestBuilder<SendEventWorker>()
.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)
}
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)
}
}
}

View File

@ -17,19 +17,20 @@
package im.vector.matrix.android.internal.session.room.send
import android.media.MediaMetadataRetriever
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.Event
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.toContent
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.message.*
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.util.StringProvider
internal class LocalEchoEventFactory(private val credentials: Credentials) {
internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) {
fun createTextEvent(roomId: String, msgType: String, text: String): Event {
val content = MessageTextContent(type = msgType, body = text)
@ -183,4 +184,80 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) {
private fun dummyEventId(roomId: String): String {
return roomId + "-" + dummyOriginServerTs()
}
fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? {
//Fallbacks and event representation
//TODO Add error/warning logs when any of this is null
val permalink = PermalinkFactory.createPermalink(eventReplied) ?: return null
val userId = eventReplied.sender ?: return null
val userLink = PermalinkFactory.createPermalink(userId) ?: return null
// <mx-reply>
// <blockquote>
// <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a>
// <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
// <br />
// <!-- This is where the related event's HTML would be. -->
// </blockquote>
// </mx-reply>
// This is where the reply goes.
val body = bodyForReply(eventReplied.content.toModel<MessageContent>())
val replyFallbackTemplateFormatted = """
<mx-reply>
<blockquote>
<a href="%s">${stringProvider.getString(R.string.in_reply_to)}</a>
<a href="%s">%s</a>
<br />
%s
</blockquote>
</mx-reply>
%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<String, String?> {
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
}
}
}

View File

@ -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() })
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Strings not defined in Riot -->
<string name="in_reply_to">In reply to</string>
<string name="sent_a_file">sent a file.</string>
<string name="sent_an_image">sent an image.</string>
<string name="sent_a_video">sent a video.</string>
<string name="sent_an_audio_file">sent an audio file.</string>
</resources>

View File

@ -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()),

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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() {

View File

@ -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(

View File

@ -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)
)

View File

@ -57,7 +57,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
//Resend and Delete
return MessageMenuState(
listOf(
SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_corner_down_right, event.root.eventId),
SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId),
//TODO delete icon
SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId)
)
@ -80,6 +80,10 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent.body))
}
if (canReply(event, messageContent)) {
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, event.root.eventId))
}
if (canEdit(event, currentSession.sessionParams.credentials.userId)) {
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, event.root.eventId))
}
@ -93,9 +97,6 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, parcel.eventId))
}
if (canReply(event, messageContent)) {
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_corner_down_right))
}
if (canShare(type)) {
if (messageContent is MessageImageContent) {
this.add(

View File

@ -38,6 +38,7 @@ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.core.linkify.VectorLinkify
import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.core.utils.DebouncedClickListener
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
@ -52,7 +53,8 @@ import me.gujun.android.span.span
class MessageItemFactory(private val colorProvider: ColorProvider,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val timelineDateFormatter: TimelineDateFormatter,
private val htmlRenderer: EventHtmlRenderer) {
private val htmlRenderer: EventHtmlRenderer,
private val stringProvider: StringProvider) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
@ -100,8 +102,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
val messageContent: MessageContent =
event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel()
?: return null
?: //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

View File

@ -1,22 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:height="13dp"
android:viewportWidth="22"
android:viewportHeight="22">
android:viewportHeight="13">
<path
android:pathData="M14.75,8.5L21,14.75 14.75,21"
android:pathData="M5.4444,1l-4.4444,4.3636l4.4444,4.3636"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeLineCap="round"/>
<path
android:pathData="M1,1v8.75a5,5 0,0 0,5 5h15"
android:pathData="M21,11.9091L21,9.7273C21,7.3173 19.0102,5.3636 16.5556,5.3636L1,5.3636"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeLineCap="round"/>
</vector>

View File

@ -17,4 +17,7 @@
<string name="event_redacted_by_admin_reason">Event moderated by room admin</string>
<string name="last_edited_info_message">Last edited by %s on %s</string>
<string name="malformed_message">Malformed event, cannot display</string>
</resources>