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/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 7c6ca39b..8603cb79 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -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 @@ -31,7 +33,12 @@ inline fun 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 + } } } 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 ae890eaf..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.ReactionService +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 @@ -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 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 deleted file mode 100644 index ace61159..00000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 im.vector.matrix.android.api.util.Cancelable - -interface ReactionService { - - - /** - * Sends a reaction (emoji) to the targetedEvent. - * @param reaction the reaction (preferably emoji) - * @param targetEventId the id of the event being reacted - */ - fun sendReaction(reaction: String, targetEventId: String): Cancelable - - - /** - * Undo a reaction (emoji) to the targetedEvent. - * @param reaction the reaction (preferably emoji) - * @param targetEventId the id of the event being reacted - * @param myUserId used to know if a reaction event was made by the user - */ - fun undoReaction(reaction: String, targetEventId: String, myUserId: String)//: Cancelable - - - /** - * Update a quick reaction (toggle). - * If you have reacted with agree and then you click on disagree, this call will delete(redact) - * the disagree and add the agree - * If you click on a reaction that you already reacted with, it will undo it - * @param reaction the reaction (preferably emoji) - * @param oppositeReaction the opposite reaction(preferably emoji) - * @param targetEventId the id of the event being reacted - * @param myUserId used to know if a reaction event was made by the user - */ - fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) - -} \ No newline at end of file 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/annotation/RelationContent.kt deleted file mode 100644 index d9e23b30..00000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/RelationContent.kt +++ /dev/null @@ -1,6 +0,0 @@ -package im.vector.matrix.android.api.session.room.model.annotation - -interface RelationContent { - val type: String? - val eventId: String? -} \ No newline at end of file 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 56% 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 bf573744..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 @@ -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 \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt new file mode 100644 index 00000000..3f60af7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt @@ -0,0 +1,7 @@ +package im.vector.matrix.android.api.session.room.model.relation + +interface RelationContent { + val type: String? + val eventId: String? + val inReplyTo: ReplyToContent? +} \ No newline at end of file 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 79% 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 7137e86a..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 @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt new file mode 100644 index 00000000..bc924728 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -0,0 +1,94 @@ +/* + * 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.relation + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.util.Cancelable + +/** + * 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 { + + + /** + * Sends a reaction (emoji) to the targetedEvent. + * @param reaction the reaction (preferably emoji) + * @param targetEventId the id of the event being reacted + */ + fun sendReaction(reaction: String, targetEventId: String): Cancelable + + + /** + * Undo a reaction (emoji) to the targetedEvent. + * @param reaction the reaction (preferably emoji) + * @param targetEventId the id of the event being reacted + * @param myUserId used to know if a reaction event was made by the user + */ + fun undoReaction(reaction: String, targetEventId: String, myUserId: String)//: Cancelable + + + /** + * Update a quick reaction (toggle). + * If you have reacted with agree and then you click on disagree, this call will delete(redact) + * the disagree and add the agree + * If you click on a reaction that you already reacted with, it will undo it + * @param reaction the reaction (preferably emoji) + * @param oppositeReaction the opposite reaction(preferably emoji) + * @param targetEventId the id of the event being reacted + * @param myUserId used to know if a reaction event was made by the user + */ + 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, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): 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/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt new file mode 100644 index 00000000..3df8a534 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt @@ -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.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReplyToContent( + @Json(name = "event_id") val eventId: String +) \ 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..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,18 @@ 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): 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 /** * Method to send a media asynchronously. @@ -49,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/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index f750c981..693c08f2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -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() 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 80d89659..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.ReactionService +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 @@ -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 by lazy { 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 c59e3f8d..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,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.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.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.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) 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 952909ef..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 @@ -73,7 +73,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - LocalEchoEventFactory(get()) + LocalEchoEventFactory(get(), get()) } scope(DefaultSession.SCOPE) { @@ -109,7 +109,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - DefaultPruneEventTask(get(),get()) as PruneEventTask + DefaultPruneEventTask(get(), get()) as PruneEventTask } } 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/relation/DefaultRelationService.kt similarity index 66% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index dbef5461..0c2addd5 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/relation/DefaultRelationService.kt @@ -13,13 +13,20 @@ * 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.ReactionService +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 +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 @@ -27,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" @@ -36,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 { @@ -150,4 +159,53 @@ internal class DefaultReactionService(private val roomId: String, .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } + + 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) + + //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) + + } + + + 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() + .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) + } + } } 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/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 28ae63b8..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,19 @@ 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) + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork) + .enqueue() + 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) 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..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 @@ -17,24 +17,79 @@ 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 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.room.model.annotation.ReactionContent -import im.vector.matrix.android.api.session.room.model.annotation.ReactionInfo +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.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 +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer -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 { + 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) } + 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, 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 = newContent.toContent() + ) + return createEvent(roomId, content) + } + fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event { return when (attachment.type) { ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) @@ -158,4 +213,76 @@ 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 +// +//
+// In reply to +// @alice:example.org +//
+// +//
+//
+// This is where the reply goes. + val body = bodyForReply(eventReplied.content.toModel()) + val replyFallbackTemplateFormatted = """ +
${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 +// +// 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) + } + + /** + * 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, + 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.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/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..02d90c2f --- /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 + +internal 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/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/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 7d6ffc04..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() @@ -31,10 +31,14 @@ 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() + 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 0278607a..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 @@ -40,6 +40,7 @@ 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 +54,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.* @@ -75,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 @@ -90,12 +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 @@ -133,13 +141,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) } } @@ -155,6 +162,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() @@ -165,6 +173,9 @@ class RoomDetailFragment : private lateinit var actionViewModel: ActionsHandler + @BindView(R.id.composerLayout) + lateinit var composerLayout: TextComposerView + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) @@ -187,6 +198,77 @@ class RoomDetailFragment : actionViewModel.actionCommandEvent.observe(this, Observer { handleActions(it) }) + + roomDetailViewModel.selectSubscribe( + RoomDetailViewState::sendMode, + RoomDetailViewState::selectedEvent, + 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, composerLayout.composerAvatarImageView) + composerLayout.collapse() + } + SendMode.EDIT, + SendMode.QUOTE, + SendMode.REPLY -> { + commandAutocompletePolicy.enabled = false + if (event == null) { + //we should ignore? can this happen? + Timber.e("Enter edit mode with no event selected") + return@selectSubscribe + } + //switch to expanded bar + 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 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) { + //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("") + 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 + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + + composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) + composerLayout.expand { + focusComposerAndShowKeyboard() + } + composerLayout.composerRelatedMessageCloseButton.setOnClickListener { + composerLayout.composerEditText.setText("") + roomDetailViewModel.resetSendMode() + } + + } + } + } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -245,8 +327,8 @@ class RoomDetailFragment : private fun setupComposer() { val elevation = 6f val backgroundDrawable = ColorDrawable(Color.WHITE) - Autocomplete.on(composerEditText) - .with(CommandAutocompletePolicy()) + Autocomplete.on(composerLayout.composerEditText) + .with(commandAutocompletePolicy) .with(autocompleteCommandPresenter) .with(elevation) .with(backgroundDrawable) @@ -265,7 +347,7 @@ class RoomDetailFragment : .build() autocompleteUserPresenter.callback = this - Autocomplete.on(composerEditText) + Autocomplete.on(composerLayout.composerEditText) .with(CharPolicy('@', true)) .with(autocompleteUserPresenter) .with(elevation) @@ -293,7 +375,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) @@ -305,16 +387,16 @@ 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)) + roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, PreferencesManager.isMarkdownEnabled(requireContext()))) } } } private fun setupAttachmentButton() { - attachmentButton.setOnClickListener { + composerLayout.attachmentButton.setOnClickListener { val intent = Intent(requireContext(), FilePickerActivity::class.java) intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder() .setCheckPermission(true) @@ -398,6 +480,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, composerLayout.composerAvatarImageView) + } else if (summary?.membership == Membership.INVITE && inviter != null) { inviteView.visibility = View.VISIBLE inviteView.render(inviter, VectorInviteView.Mode.LARGE) @@ -428,7 +515,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)) @@ -601,6 +688,18 @@ class RoomDetailFragment : roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite)) } } + MessageMenuViewModel.ACTION_EDIT -> { + val eventId = actionData.data.toString() + roomDetailViewModel.process(RoomDetailActions.EnterEditMode(eventId)) + } + MessageMenuViewModel.ACTION_QUOTE -> { + 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() } @@ -614,6 +713,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) { @@ -622,21 +722,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 @@ -648,12 +748,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() { + composerLayout.composerEditText.requestFocus() + val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showSoftInput(composerLayout.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..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 @@ -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,29 @@ 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) + is RoomDetailActions.EnterReplyMode -> handleReplyAction(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 +130,145 @@ 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, autoMarkdown = action.autoMarkdown) + _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, action.autoMarkdown) + setState { + copy( + sendMode = SendMode.REGULAR, + selectedEvent = null + ) + } + _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) + } + SendMode.QUOTE -> { + 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)) + } + SendMode.REPLY -> { + state.selectedEvent?.let { + room.replyToMessage(it.root, action.text) + 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 +372,34 @@ 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 handleReplyAction(action: RoomDetailActions.EnterReplyMode) { + room.getTimeLineEvent(action.eventId)?.let { + setState { + copy( + sendMode = SendMode.REPLY, + 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..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 @@ -22,15 +22,33 @@ 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, + REPLY +} + 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/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/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 c39da484..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,16 +50,17 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel().apply { - this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_smile, event.root.eventId)) + + 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 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)) + } + 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)) { @@ -82,9 +98,6 @@ 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 +209,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel.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 diff --git a/vector/src/main/res/drawable-hdpi/ic_attach_file_white.png b/vector/src/main/res/drawable-hdpi/ic_attach_file_white.png deleted file mode 100644 index 06f642be..00000000 Binary files a/vector/src/main/res/drawable-hdpi/ic_attach_file_white.png and /dev/null differ 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 f133cdbe..00000000 Binary files a/vector/src/main/res/drawable-hdpi/ic_send_white.png and /dev/null differ 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 a819f403..00000000 Binary files a/vector/src/main/res/drawable-mdpi/ic_attach_file_white.png and /dev/null differ 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 34e49af7..00000000 Binary files a/vector/src/main/res/drawable-mdpi/ic_send_white.png and /dev/null differ 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 c572c359..00000000 Binary files a/vector/src/main/res/drawable-xhdpi/ic_attach_file_white.png and /dev/null differ 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 e5f9ba41..00000000 Binary files a/vector/src/main/res/drawable-xhdpi/ic_send_white.png and /dev/null differ 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 a5dc29f0..00000000 Binary files a/vector/src/main/res/drawable-xxhdpi/ic_attach_file_white.png and /dev/null differ 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 0ba718b6..00000000 Binary files a/vector/src/main/res/drawable-xxhdpi/ic_send_white.png and /dev/null differ 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 60147bc7..00000000 Binary files a/vector/src/main/res/drawable-xxxhdpi/ic_attach_file_white.png and /dev/null differ 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 f02b6453..00000000 Binary files a/vector/src/main/res/drawable-xxxhdpi/ic_send_white.png and /dev/null differ diff --git a/vector/src/main/res/drawable/ic_add_reaction.xml b/vector/src/main/res/drawable/ic_add_reaction.xml new file mode 100644 index 00000000..ba33bc3f --- /dev/null +++ b/vector/src/main/res/drawable/ic_add_reaction.xml @@ -0,0 +1,54 @@ + + + + + + + + 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_corner_down_right.xml b/vector/src/main/res/drawable/ic_reply.xml similarity index 72% rename from vector/src/main/res/drawable/ic_corner_down_right.xml rename to vector/src/main/res/drawable/ic_reply.xml index 109222a3..924dda82 100644 --- a/vector/src/main/res/drawable/ic_corner_down_right.xml +++ b/vector/src/main/res/drawable/ic_reply.xml @@ -1,22 +1,22 @@ + android:viewportHeight="13"> 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..48048e62 --- /dev/null +++ b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 c65ae0e0..2e03a88d 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 @@ @@ -76,69 +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_base" /> - - - - - - - - - - + app:layout_constraintStart_toStartOf="parent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 5a37c393..d00b0d6f 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -18,10 +18,9 @@ Last edited by %s on %s + Malformed event, cannot display Create New Room - No network. Please check your Internet connection. - "Change" \ No newline at end of file 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