Merge branch 'develop' into feature/home_rework
@ -48,7 +48,7 @@ android {
|
|||||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
||||||
|
|
||||||
// Set to BODY instead of NONE to enable logging
|
// Set to BODY instead of NONE to enable logging
|
||||||
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
|
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BASIC"
|
||||||
}
|
}
|
||||||
|
|
||||||
release {
|
release {
|
||||||
@ -91,6 +91,7 @@ dependencies {
|
|||||||
def moshi_version = '1.8.0'
|
def moshi_version = '1.8.0'
|
||||||
def lifecycle_version = '2.0.0'
|
def lifecycle_version = '2.0.0'
|
||||||
def coroutines_version = "1.0.1"
|
def coroutines_version = "1.0.1"
|
||||||
|
def markwon_version = '3.0.0-SNAPSHOT'
|
||||||
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.aar'])
|
implementation fileTree(dir: 'libs', include: ['*.aar'])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
@ -112,6 +113,8 @@ dependencies {
|
|||||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
||||||
|
|
||||||
|
implementation "ru.noties.markwon:core:$markwon_version"
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
|
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
|
||||||
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
||||||
|
@ -18,12 +18,10 @@ package im.vector.matrix.android.session.room.timeline
|
|||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.InstrumentedTest
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.internal.database.model.SessionRealmModule
|
import im.vector.matrix.android.internal.database.model.SessionRealmModule
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
|
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
|
||||||
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
|
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||||
@ -60,8 +58,7 @@ internal class TimelineTest : InstrumentedTest {
|
|||||||
|
|
||||||
private fun createTimeline(initialEventId: String? = null): Timeline {
|
private fun createTimeline(initialEventId: String? = null): Timeline {
|
||||||
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
|
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
|
||||||
val erau = EventRelationsAggregationUpdater(Credentials("", "", "", null, null))
|
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
|
||||||
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy, erau)
|
|
||||||
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
||||||
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
||||||
val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
|
val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
|
||||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
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.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.read.ReadService
|
||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
import im.vector.matrix.android.api.session.room.send.SendService
|
||||||
import im.vector.matrix.android.api.session.room.state.StateService
|
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.
|
* 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
|
* The roomId of this room
|
||||||
|
@ -21,5 +21,6 @@ data class EditAggregatedSummary(
|
|||||||
val aggregatedContent: Content? = null,
|
val aggregatedContent: Content? = null,
|
||||||
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||||
val sourceEvents: List<String>,
|
val sourceEvents: List<String>,
|
||||||
|
val localEchos: List<String>,
|
||||||
val lastEditTs: Long = 0
|
val lastEditTs: Long = 0
|
||||||
)
|
)
|
||||||
|
@ -5,5 +5,6 @@ data class ReactionAggregatedSummary(
|
|||||||
val count: Int, // 8
|
val count: Int, // 8
|
||||||
val addedByMe: Boolean, // true
|
val addedByMe: Boolean, // true
|
||||||
val firstTimestamp: Long, // unix timestamp
|
val firstTimestamp: Long, // unix timestamp
|
||||||
val sourceEvents: List<String>
|
val sourceEvents: List<String>,
|
||||||
|
val localEchoEvents: List<String>
|
||||||
)
|
)
|
@ -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)
|
|
||||||
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package im.vector.matrix.android.api.session.room.model.annotation
|
|
||||||
|
|
||||||
interface RelationContent {
|
|
||||||
val type: String?
|
|
||||||
val eventId: String?
|
|
||||||
}
|
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageAudioContent(
|
data class MessageAudioContent(
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
package im.vector.matrix.android.api.session.room.model.message
|
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.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 {
|
interface MessageContent {
|
||||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageDefaultContent(
|
data class MessageDefaultContent(
|
||||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageEmoteContent(
|
data class MessageEmoteContent(
|
||||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageFileContent(
|
data class MessageFileContent(
|
||||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageImageContent(
|
data class MessageImageContent(
|
||||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageLocationContent(
|
data class MessageLocationContent(
|
||||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageNoticeContent(
|
data class MessageNoticeContent(
|
||||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageTextContent(
|
data class MessageTextContent(
|
||||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageVideoContent(
|
data class MessageVideoContent(
|
||||||
|
@ -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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
@ -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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
@ -7,5 +7,7 @@ import com.squareup.moshi.JsonClass
|
|||||||
data class ReactionInfo(
|
data class ReactionInfo(
|
||||||
@Json(name = "rel_type") override val type: String?,
|
@Json(name = "rel_type") override val type: String?,
|
||||||
@Json(name = "event_id") override val eventId: 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
|
) : RelationContent
|
@ -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?
|
||||||
|
}
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class RelationDefaultContent(
|
data class RelationDefaultContent(
|
||||||
@Json(name = "rel_type") override val type: String?,
|
@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
|
) : RelationContent
|
@ -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?
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
@ -31,9 +31,18 @@ interface SendService {
|
|||||||
* Method to send a text message asynchronously.
|
* Method to send a text message asynchronously.
|
||||||
* @param text the text message to send
|
* @param text the text message to send
|
||||||
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
|
* @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]
|
* @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.
|
* Method to send a media asynchronously.
|
||||||
@ -49,6 +58,11 @@ interface SendService {
|
|||||||
*/
|
*/
|
||||||
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
|
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redacts (delete) the given event.
|
||||||
|
* @param event The event to redact
|
||||||
|
* @param reason Optional reason string
|
||||||
|
*/
|
||||||
fun redactEvent(event: Event, reason: String?): Cancelable
|
fun redactEvent(event: Event, reason: String?): Cancelable
|
||||||
|
|
||||||
}
|
}
|
@ -15,13 +15,15 @@ internal object EventAnnotationsSummaryMapper {
|
|||||||
it.count,
|
it.count,
|
||||||
it.addedByMe,
|
it.addedByMe,
|
||||||
it.firstTimestamp,
|
it.firstTimestamp,
|
||||||
it.sourceEvents.toList()
|
it.sourceEvents.toList(),
|
||||||
|
it.sourceLocalEcho.toList()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
editSummary = annotationsSummary.editSummary?.let {
|
editSummary = annotationsSummary.editSummary?.let {
|
||||||
EditAggregatedSummary(
|
EditAggregatedSummary(
|
||||||
ContentMapper.map(it.aggregatedContent),
|
ContentMapper.map(it.aggregatedContent),
|
||||||
it.sourceEvents.toList(),
|
it.sourceEvents.toList(),
|
||||||
|
it.sourceLocalEchoEvents.toList(),
|
||||||
it.lastEditTs
|
it.lastEditTs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ internal open class EditAggregatedSummaryEntity(
|
|||||||
var aggregatedContent: String? = null,
|
var aggregatedContent: String? = null,
|
||||||
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||||
var sourceEvents: RealmList<String> = RealmList(),
|
var sourceEvents: RealmList<String> = RealmList(),
|
||||||
|
var sourceLocalEchoEvents: RealmList<String> = RealmList(),
|
||||||
var lastEditTs: Long = 0
|
var lastEditTs: Long = 0
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
|
@ -16,7 +16,9 @@ internal open class ReactionAggregatedSummaryEntity(
|
|||||||
// The first time this reaction was added (for ordering purpose)
|
// The first time this reaction was added (for ordering purpose)
|
||||||
var firstTimestamp: Long = 0,
|
var firstTimestamp: Long = 0,
|
||||||
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||||
var sourceEvents: RealmList<String> = RealmList()
|
var sourceEvents: RealmList<String> = RealmList(),
|
||||||
|
// List of transaction ids for local echos
|
||||||
|
var sourceLocalEcho: RealmList<String> = RealmList()
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
companion object
|
companion object
|
||||||
|
@ -48,6 +48,7 @@ internal fun EventEntity.Companion.where(realm: Realm,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal fun EventEntity.Companion.latestEvent(realm: Realm,
|
internal fun EventEntity.Companion.latestEvent(realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
includedTypes: List<String> = emptyList(),
|
includedTypes: List<String> = emptyList(),
|
||||||
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
|
import im.vector.matrix.android.internal.util.StringProvider
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.koin.dsl.module.module
|
import org.koin.dsl.module.module
|
||||||
|
|
||||||
@ -39,6 +40,9 @@ class MatrixModule(private val context: Context) {
|
|||||||
single {
|
single {
|
||||||
TaskExecutor(get())
|
TaskExecutor(get())
|
||||||
}
|
}
|
||||||
|
single {
|
||||||
|
StringProvider(context.resources)
|
||||||
|
}
|
||||||
|
|
||||||
single {
|
single {
|
||||||
BackgroundDetectionObserver()
|
BackgroundDetectionObserver()
|
||||||
|
@ -105,10 +105,6 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
|||||||
RoomSummaryUpdater(get(), get(), get())
|
RoomSummaryUpdater(get(), get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
|
||||||
EventRelationsAggregationUpdater(get())
|
|
||||||
}
|
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
DefaultRoomService(get(), get(), get(), get()) as RoomService
|
DefaultRoomService(get(), get(), get(), get()) as RoomService
|
||||||
}
|
}
|
||||||
@ -168,9 +164,11 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
|||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
val groupSummaryUpdater = GroupSummaryUpdater(get())
|
val groupSummaryUpdater = GroupSummaryUpdater(get())
|
||||||
val eventsPruner = EventsPruner(get(), get(), get(), get())
|
|
||||||
val userEntityUpdater = UserEntityUpdater(get(), get(), get())
|
val userEntityUpdater = UserEntityUpdater(get(), get(), get())
|
||||||
listOf<LiveEntityObserver>(groupSummaryUpdater, eventsPruner, userEntityUpdater)
|
val aggregationUpdater = EventRelationsAggregationUpdater(get(), get(), get(), get())
|
||||||
|
//Event pruner must be the last one, because it will clear contents
|
||||||
|
val eventsPruner = EventsPruner(get(), get(), get(), get())
|
||||||
|
listOf<LiveEntityObserver>(groupSummaryUpdater, userEntityUpdater, aggregationUpdater, eventsPruner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.Room
|
||||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
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.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.read.ReadService
|
||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
import im.vector.matrix.android.api.session.room.send.SendService
|
||||||
import im.vector.matrix.android.api.session.room.state.StateService
|
import im.vector.matrix.android.api.session.room.state.StateService
|
||||||
@ -40,14 +40,14 @@ internal class DefaultRoom(
|
|||||||
private val sendService: SendService,
|
private val sendService: SendService,
|
||||||
private val stateService: StateService,
|
private val stateService: StateService,
|
||||||
private val readService: ReadService,
|
private val readService: ReadService,
|
||||||
private val reactionService: ReactionService,
|
private val relationService: RelationService,
|
||||||
private val roomMembersService: MembershipService
|
private val roomMembersService: MembershipService
|
||||||
) : Room,
|
) : Room,
|
||||||
TimelineService by timelineService,
|
TimelineService by timelineService,
|
||||||
SendService by sendService,
|
SendService by sendService,
|
||||||
StateService by stateService,
|
StateService by stateService,
|
||||||
ReadService by readService,
|
ReadService by readService,
|
||||||
ReactionService by reactionService,
|
RelationService by relationService,
|
||||||
MembershipService by roomMembersService {
|
MembershipService by roomMembersService {
|
||||||
|
|
||||||
override val roomSummary: LiveData<RoomSummary> by lazy {
|
override val roomSummary: LiveData<RoomSummary> by lazy {
|
||||||
|
@ -0,0 +1,309 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.session.room
|
||||||
|
|
||||||
|
import arrow.core.Try
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.session.events.model.*
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||||
|
import im.vector.matrix.android.internal.database.model.*
|
||||||
|
import im.vector.matrix.android.internal.database.query.create
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||||
|
import io.realm.Realm
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
internal interface EventRelationsAggregationTask : Task<EventRelationsAggregationTask.Params, Unit> {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val events: List<Pair<Event, SendState>>,
|
||||||
|
val userId: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
|
||||||
|
*/
|
||||||
|
internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarchy) : EventRelationsAggregationTask {
|
||||||
|
|
||||||
|
override fun execute(params: EventRelationsAggregationTask.Params): Try<Unit> {
|
||||||
|
return monarchy.tryTransactionAsync { realm ->
|
||||||
|
update(realm, params.events, params.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun update(realm: Realm, events: List<Pair<Event, SendState>>, userId: String) {
|
||||||
|
events.forEach { pair ->
|
||||||
|
val roomId = pair.first.roomId ?: return@forEach
|
||||||
|
val event = pair.first
|
||||||
|
val sendState = pair.second
|
||||||
|
val isLocalEcho = sendState == SendState.UNSENT
|
||||||
|
when (event.type) {
|
||||||
|
EventType.REACTION -> {
|
||||||
|
//we got a reaction!!
|
||||||
|
Timber.v("###REACTION in room $roomId")
|
||||||
|
handleReaction(event, roomId, realm, userId, isLocalEcho)
|
||||||
|
}
|
||||||
|
EventType.MESSAGE -> {
|
||||||
|
if (event.unsignedData?.relations?.annotations != null) {
|
||||||
|
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
||||||
|
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
|
||||||
|
} else {
|
||||||
|
val content: MessageContent? = event.content.toModel()
|
||||||
|
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||||
|
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||||
|
//A replace!
|
||||||
|
handleReplace(realm, event, content, roomId, isLocalEcho)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
EventType.REDACTION -> {
|
||||||
|
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||||
|
?: return
|
||||||
|
when (eventToPrune.type) {
|
||||||
|
EventType.MESSAGE -> {
|
||||||
|
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||||
|
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||||
|
?: UnsignedData(null, null)
|
||||||
|
|
||||||
|
//was this event a m.replace
|
||||||
|
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
||||||
|
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
||||||
|
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
EventType.REACTION -> {
|
||||||
|
handleReactionRedact(eventToPrune, realm, userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) {
|
||||||
|
val eventId = event.eventId ?: return
|
||||||
|
val targetEventId = content.relatesTo?.eventId ?: return
|
||||||
|
val newContent = content.newContent ?: return
|
||||||
|
//ok, this is a replace
|
||||||
|
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
|
||||||
|
if (existing == null) {
|
||||||
|
Timber.v("###REPLACE creating no relation summary for ${targetEventId}")
|
||||||
|
existing = EventAnnotationsSummaryEntity.create(realm, targetEventId)
|
||||||
|
existing.roomId = roomId
|
||||||
|
}
|
||||||
|
|
||||||
|
//we have it
|
||||||
|
val existingSummary = existing.editSummary
|
||||||
|
if (existingSummary == null) {
|
||||||
|
Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one (localEcho:$isLocalEcho)")
|
||||||
|
//create the edit summary
|
||||||
|
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
|
||||||
|
editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
||||||
|
editSummary.aggregatedContent = ContentMapper.map(newContent)
|
||||||
|
if (isLocalEcho) {
|
||||||
|
editSummary.sourceLocalEchoEvents.add(eventId)
|
||||||
|
} else {
|
||||||
|
editSummary.sourceEvents.add(eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.editSummary = editSummary
|
||||||
|
} else {
|
||||||
|
if (existingSummary.sourceEvents.contains(eventId)) {
|
||||||
|
//ignore this event, we already know it (??)
|
||||||
|
Timber.v("###REPLACE ignoring event for summary, it's known ${eventId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val txId = event.unsignedData?.transactionId
|
||||||
|
//is it a remote echo?
|
||||||
|
if (!isLocalEcho && existingSummary.sourceLocalEchoEvents.contains(txId)) {
|
||||||
|
//ok it has already been managed
|
||||||
|
Timber.v("###REPLACE Receiving remote echo of edit (edit already done)")
|
||||||
|
existingSummary.sourceLocalEchoEvents.remove(txId)
|
||||||
|
existingSummary.sourceEvents.add(event.eventId)
|
||||||
|
} else if (event.originServerTs ?: 0 > existingSummary.lastEditTs) {
|
||||||
|
Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
|
||||||
|
existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
||||||
|
existingSummary.aggregatedContent = ContentMapper.map(newContent)
|
||||||
|
existingSummary.sourceEvents.add(eventId)
|
||||||
|
} else {
|
||||||
|
//ignore this event for the summary
|
||||||
|
Timber.v("###REPLACE ignoring event for summary, it's to old ${eventId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
|
||||||
|
aggregation.chunk?.forEach {
|
||||||
|
if (it.type == EventType.REACTION) {
|
||||||
|
val eventId = event.eventId ?: ""
|
||||||
|
val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
if (existing == null) {
|
||||||
|
val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId)
|
||||||
|
eventSummary.roomId = roomId
|
||||||
|
val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||||
|
sum.key = it.key
|
||||||
|
sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order?
|
||||||
|
sum.count = it.count
|
||||||
|
eventSummary.reactionsSummary.add(sum)
|
||||||
|
} else {
|
||||||
|
//TODO how to handle that
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) {
|
||||||
|
event.content.toModel<ReactionContent>()?.let { content ->
|
||||||
|
//rel_type must be m.annotation
|
||||||
|
if (RelationType.ANNOTATION == content.relatesTo?.type) {
|
||||||
|
val reaction = content.relatesTo.key
|
||||||
|
val eventId = content.relatesTo.eventId
|
||||||
|
val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId }
|
||||||
|
|
||||||
|
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
|
||||||
|
val txId = event.unsignedData?.transactionId
|
||||||
|
if (isLocalEcho && txId.isNullOrBlank()) {
|
||||||
|
Timber.w("Received a local echo with no transaction ID")
|
||||||
|
}
|
||||||
|
if (sum == null) {
|
||||||
|
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||||
|
sum.key = reaction
|
||||||
|
sum.firstTimestamp = event.originServerTs ?: 0
|
||||||
|
if (isLocalEcho) {
|
||||||
|
Timber.v("Adding local echo reaction $reaction")
|
||||||
|
sum.sourceLocalEcho.add(txId)
|
||||||
|
sum.count = 1
|
||||||
|
} else {
|
||||||
|
Timber.v("Adding synced reaction $reaction")
|
||||||
|
sum.count = 1
|
||||||
|
sum.sourceEvents.add(event.eventId)
|
||||||
|
}
|
||||||
|
sum.addedByMe = sum.addedByMe || (userId == event.sender)
|
||||||
|
eventSummary.reactionsSummary.add(sum)
|
||||||
|
} else {
|
||||||
|
//is this a known event (is possible? pagination?)
|
||||||
|
if (!sum.sourceEvents.contains(eventId)) {
|
||||||
|
|
||||||
|
//check if it's not the sync of a local echo
|
||||||
|
if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) {
|
||||||
|
//ok it has already been counted, just sync the list, do not touch count
|
||||||
|
Timber.v("Ignoring synced of local echo for reaction $reaction")
|
||||||
|
sum.sourceLocalEcho.remove(txId)
|
||||||
|
sum.sourceEvents.add(event.eventId)
|
||||||
|
} else {
|
||||||
|
sum.count += 1
|
||||||
|
if (isLocalEcho) {
|
||||||
|
Timber.v("Adding local echo reaction $reaction")
|
||||||
|
sum.sourceLocalEcho.add(txId)
|
||||||
|
} else {
|
||||||
|
Timber.v("Adding synced reaction $reaction")
|
||||||
|
sum.sourceEvents.add(event.eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
sum.addedByMe = sum.addedByMe || (userId == event.sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an event is deleted
|
||||||
|
*/
|
||||||
|
private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
|
||||||
|
Timber.d("Handle redaction of m.replace")
|
||||||
|
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst()
|
||||||
|
if (eventSummary == null) {
|
||||||
|
Timber.w("Redaction of a replace targeting an unknown event $relatedEventId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val sourceEvents = eventSummary.editSummary?.sourceEvents
|
||||||
|
val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId)
|
||||||
|
if (sourceToDiscard == null) {
|
||||||
|
Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//Need to remove this event from the redaction list and compute new aggregation state
|
||||||
|
sourceEvents.removeAt(sourceToDiscard)
|
||||||
|
val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull()
|
||||||
|
if (previousEdit == null) {
|
||||||
|
//revert to original
|
||||||
|
eventSummary.editSummary?.deleteFromRealm()
|
||||||
|
} else {
|
||||||
|
//I have the last event
|
||||||
|
ContentMapper.map(previousEdit.content)?.toModel<MessageContent>()?.newContent?.let { newContent ->
|
||||||
|
eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs
|
||||||
|
?: System.currentTimeMillis()
|
||||||
|
eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent)
|
||||||
|
} ?: run {
|
||||||
|
Timber.e("Failed to udate edited summary")
|
||||||
|
//TODO how to reccover that
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
|
||||||
|
Timber.v("REDACTION of reaction ${eventToPrune.eventId}")
|
||||||
|
//delete a reaction, need to update the annotation summary if any
|
||||||
|
val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel()
|
||||||
|
?: return
|
||||||
|
val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return
|
||||||
|
|
||||||
|
val reactionKey = reactionContent.relatesTo.key
|
||||||
|
Timber.v("REMOVE reaction for key $reactionKey")
|
||||||
|
val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst()
|
||||||
|
if (summary != null) {
|
||||||
|
summary.reactionsSummary.where()
|
||||||
|
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionKey)
|
||||||
|
.findFirst()?.let { aggregation ->
|
||||||
|
Timber.v("Find summary for key with ${aggregation.sourceEvents.size} known reactions (count:${aggregation.count})")
|
||||||
|
Timber.v("Known reactions ${aggregation.sourceEvents.joinToString(",")}")
|
||||||
|
if (aggregation.sourceEvents.contains(eventToPrune.eventId)) {
|
||||||
|
Timber.v("REMOVE reaction for key $reactionKey")
|
||||||
|
aggregation.sourceEvents.remove(eventToPrune.eventId)
|
||||||
|
Timber.v("Known reactions after ${aggregation.sourceEvents.joinToString(",")}")
|
||||||
|
aggregation.count = aggregation.count - 1
|
||||||
|
if (eventToPrune.sender == userId) {
|
||||||
|
//Was it a redact on my reaction?
|
||||||
|
aggregation.addedByMe = false
|
||||||
|
}
|
||||||
|
if (aggregation.count == 0) {
|
||||||
|
//delete!
|
||||||
|
aggregation.deleteFromRealm()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("## Cannot find summary for key $reactionKey")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,16 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.session.room
|
package im.vector.matrix.android.internal.session.room
|
||||||
|
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.events.model.*
|
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||||
import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
|
||||||
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
|
||||||
import im.vector.matrix.android.internal.database.model.*
|
|
||||||
import im.vector.matrix.android.internal.database.query.create
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import io.realm.Realm
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,198 +30,34 @@ import timber.log.Timber
|
|||||||
* For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity.
|
* For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity.
|
||||||
* The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display.
|
* The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display.
|
||||||
*/
|
*/
|
||||||
internal class EventRelationsAggregationUpdater(private val credentials: Credentials) {
|
internal class EventRelationsAggregationUpdater(monarchy: Monarchy,
|
||||||
|
private val credentials: Credentials,
|
||||||
|
private val task: EventRelationsAggregationTask,
|
||||||
|
private val taskExecutor: TaskExecutor) :
|
||||||
|
RealmLiveEntityObserver<EventEntity>(monarchy) {
|
||||||
|
|
||||||
fun update(realm: Realm, roomId: String, events: List<Event>?) {
|
override val query = Monarchy.Query<EventEntity> {
|
||||||
events?.forEach { event ->
|
EventEntity.where(it)
|
||||||
when (event.type) {
|
//mmm why is this query not working?
|
||||||
EventType.REACTION -> {
|
// EventEntity.byTypes(it, listOf(
|
||||||
//we got a reaction!!
|
// EventType.REDACTION, EventType.MESSAGE, EventType.REDACTION)
|
||||||
Timber.v("###REACTION in room $roomId")
|
// )
|
||||||
handleReaction(event, roomId, realm)
|
|
||||||
}
|
|
||||||
EventType.MESSAGE -> {
|
|
||||||
if (event.unsignedData?.relations?.annotations != null) {
|
|
||||||
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
|
||||||
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
|
|
||||||
} else {
|
|
||||||
val content: MessageContent? = event.content.toModel()
|
|
||||||
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
|
||||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
|
||||||
//A replace!
|
|
||||||
handleReplace(event, content, roomId, realm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleReplace(event: Event, content: MessageContent, roomId: String, realm: Realm) {
|
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||||
val eventId = event.eventId ?: return
|
Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions")
|
||||||
val targetEventId = content.relatesTo?.eventId ?: return
|
val inserted = inserted
|
||||||
val newContent = content.newContent ?: return
|
.mapNotNull { it.asDomain() to it.sendState }
|
||||||
//ok, this is a replace
|
|
||||||
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
|
|
||||||
if (existing == null) {
|
|
||||||
Timber.v("###REPLACE creating no relation summary for ${targetEventId}")
|
|
||||||
existing = EventAnnotationsSummaryEntity.create(realm, targetEventId)
|
|
||||||
existing.roomId = roomId
|
|
||||||
}
|
|
||||||
|
|
||||||
//we have it
|
val params = EventRelationsAggregationTask.Params(
|
||||||
val existingSummary = existing.editSummary
|
inserted,
|
||||||
if (existingSummary == null) {
|
credentials.userId
|
||||||
Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one")
|
)
|
||||||
//create the edit summary
|
|
||||||
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
|
|
||||||
editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
|
||||||
editSummary.aggregatedContent = ContentMapper.map(newContent)
|
|
||||||
editSummary.sourceEvents.add(eventId)
|
|
||||||
|
|
||||||
existing.editSummary = editSummary
|
task.configureWith(params)
|
||||||
} else {
|
.executeBy(taskExecutor)
|
||||||
if (existingSummary.sourceEvents.contains(eventId)) {
|
|
||||||
//ignore this event, we already know it (??)
|
|
||||||
Timber.v("###REPLACE ignoring event for summary, it's known ${eventId}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//This message has already been edited
|
|
||||||
if (event.originServerTs ?: 0 > existingSummary.lastEditTs ?: 0) {
|
|
||||||
Timber.v("###REPLACE Computing aggregated edit summary")
|
|
||||||
existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
|
||||||
existingSummary.aggregatedContent = ContentMapper.map(newContent)
|
|
||||||
existingSummary.sourceEvents.add(eventId)
|
|
||||||
} else {
|
|
||||||
//ignore this event for the summary
|
|
||||||
Timber.v("###REPLACE ignoring event for summary, it's to old ${eventId}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
|
|
||||||
aggregation.chunk?.forEach {
|
|
||||||
if (it.type == EventType.REACTION) {
|
|
||||||
val eventId = event.eventId ?: ""
|
|
||||||
val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
|
||||||
if (existing == null) {
|
|
||||||
val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId)
|
|
||||||
eventSummary.roomId = roomId
|
|
||||||
val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
|
||||||
sum.key = it.key
|
|
||||||
sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order?
|
|
||||||
sum.count = it.count
|
|
||||||
eventSummary.reactionsSummary.add(sum)
|
|
||||||
} else {
|
|
||||||
//TODO how to handle that
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleReaction(event: Event, roomId: String, realm: Realm) {
|
|
||||||
event.content.toModel<ReactionContent>()?.let { content ->
|
|
||||||
//rel_type must be m.annotation
|
|
||||||
if (RelationType.ANNOTATION == content.relatesTo?.type) {
|
|
||||||
val reaction = content.relatesTo.key
|
|
||||||
val eventId = content.relatesTo.eventId
|
|
||||||
val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
|
||||||
?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId }
|
|
||||||
|
|
||||||
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
|
|
||||||
if (sum == null) {
|
|
||||||
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
|
||||||
sum.key = reaction
|
|
||||||
sum.firstTimestamp = event.originServerTs ?: 0
|
|
||||||
sum.count = 1
|
|
||||||
sum.sourceEvents.add(event.eventId)
|
|
||||||
sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender)
|
|
||||||
eventSummary.reactionsSummary.add(sum)
|
|
||||||
} else {
|
|
||||||
//is this a known event (is possible? pagination?)
|
|
||||||
if (!sum.sourceEvents.contains(eventId)) {
|
|
||||||
sum.count += 1
|
|
||||||
sum.sourceEvents.add(event.eventId)
|
|
||||||
sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when an event is deleted
|
|
||||||
*/
|
|
||||||
fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
|
|
||||||
Timber.d("Handle redaction of m.replace")
|
|
||||||
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst()
|
|
||||||
if (eventSummary == null) {
|
|
||||||
Timber.w("Redaction of a replace targeting an unknown event $relatedEventId")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val sourceEvents = eventSummary.editSummary?.sourceEvents
|
|
||||||
val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId)
|
|
||||||
if (sourceToDiscard == null) {
|
|
||||||
Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//Need to remove this event from the redaction list and compute new aggregation state
|
|
||||||
sourceEvents.removeAt(sourceToDiscard)
|
|
||||||
val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull()
|
|
||||||
if (previousEdit == null) {
|
|
||||||
//revert to original
|
|
||||||
eventSummary.editSummary?.deleteFromRealm()
|
|
||||||
} else {
|
|
||||||
//I have the last event
|
|
||||||
ContentMapper.map(previousEdit.content)?.toModel<MessageContent>()?.newContent?.let { newContent ->
|
|
||||||
eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs
|
|
||||||
?: System.currentTimeMillis()
|
|
||||||
eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent)
|
|
||||||
} ?: run {
|
|
||||||
Timber.e("Failed to udate edited summary")
|
|
||||||
//TODO how to reccover that
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
|
|
||||||
Timber.d("REDACTION of reaction ${eventToPrune.eventId}")
|
|
||||||
//delete a reaction, need to update the annotation summary if any
|
|
||||||
val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel()
|
|
||||||
?: return
|
|
||||||
val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return
|
|
||||||
|
|
||||||
val reactionkey = reactionContent.relatesTo.key
|
|
||||||
Timber.d("REMOVE reaction for key $reactionkey")
|
|
||||||
val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst()
|
|
||||||
if (summary != null) {
|
|
||||||
summary.reactionsSummary.where()
|
|
||||||
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionkey)
|
|
||||||
.findFirst()?.let { summary ->
|
|
||||||
Timber.d("Find summary for key with ${summary.sourceEvents.size} known reactions (count:${summary.count})")
|
|
||||||
Timber.d("Known reactions ${summary.sourceEvents.joinToString(",")}")
|
|
||||||
if (summary.sourceEvents.contains(eventToPrune.eventId)) {
|
|
||||||
Timber.d("REMOVE reaction for key $reactionkey")
|
|
||||||
summary.sourceEvents.remove(eventToPrune.eventId)
|
|
||||||
Timber.d("Known reactions after ${summary.sourceEvents.joinToString(",")}")
|
|
||||||
summary.count = summary.count - 1
|
|
||||||
if (eventToPrune.sender == userId) {
|
|
||||||
//Was it a redact on my reaction?
|
|
||||||
summary.addedByMe = false
|
|
||||||
}
|
|
||||||
if (summary.count == 0) {
|
|
||||||
//delete!
|
|
||||||
summary.deleteFromRealm()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.e("## Cannot find summary for key $reactionkey")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,10 +18,10 @@ package im.vector.matrix.android.internal.session.room
|
|||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
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.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.LoadRoomMembersTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
|
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
|
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 timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor())
|
||||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask)
|
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask)
|
||||||
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
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 stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
|
||||||
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
||||||
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask)
|
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask)
|
||||||
|
@ -17,10 +17,6 @@
|
|||||||
package im.vector.matrix.android.internal.session.room
|
package im.vector.matrix.android.internal.session.room
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.session.DefaultSession
|
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.create.CreateRoomTask
|
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.create.DefaultCreateRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask
|
import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask
|
||||||
@ -35,6 +31,10 @@ import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTas
|
|||||||
import im.vector.matrix.android.internal.session.room.prune.PruneEventTask
|
import im.vector.matrix.android.internal.session.room.prune.PruneEventTask
|
||||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
||||||
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask
|
||||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||||
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
|
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
|
||||||
import im.vector.matrix.android.internal.session.room.state.SendStateTask
|
import im.vector.matrix.android.internal.session.room.state.SendStateTask
|
||||||
@ -57,7 +57,7 @@ class RoomModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
TokenChunkEventPersistor(get(), get())
|
TokenChunkEventPersistor(get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
@ -73,7 +73,7 @@ class RoomModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
LocalEchoEventFactory(get())
|
LocalEchoEventFactory(get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
@ -109,7 +109,11 @@ class RoomModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
DefaultPruneEventTask(get(),get()) as PruneEventTask
|
DefaultPruneEventTask(get()) as PruneEventTask
|
||||||
|
}
|
||||||
|
|
||||||
|
scope(DefaultSession.SCOPE) {
|
||||||
|
DefaultEventRelationsAggregationTask(get()) as EventRelationsAggregationTask
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,153 +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.internal.session.room.annotation
|
|
||||||
|
|
||||||
import androidx.work.*
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
|
||||||
import im.vector.matrix.android.api.session.room.model.annotation.ReactionService
|
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
|
||||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
|
||||||
import im.vector.matrix.android.internal.session.room.send.RedactEventWorker
|
|
||||||
import im.vector.matrix.android.internal.session.room.send.SendEventWorker
|
|
||||||
import im.vector.matrix.android.internal.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 java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
private const val REACTION_WORK = "REACTION_WORK"
|
|
||||||
private const val BACKOFF_DELAY = 10_000L
|
|
||||||
|
|
||||||
private val WORK_CONSTRAINTS = Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
internal class DefaultReactionService(private val roomId: String,
|
|
||||||
private val eventFactory: LocalEchoEventFactory,
|
|
||||||
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
|
||||||
private val updateQuickReactionTask: UpdateQuickReactionTask,
|
|
||||||
private val taskExecutor: TaskExecutor)
|
|
||||||
: ReactionService {
|
|
||||||
|
|
||||||
|
|
||||||
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
|
|
||||||
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
|
||||||
// .also {
|
|
||||||
// //saveLocalEcho(it)
|
|
||||||
// }
|
|
||||||
val sendRelationWork = createSendRelationWork(event)
|
|
||||||
WorkManager.getInstance()
|
|
||||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, sendRelationWork)
|
|
||||||
.enqueue()
|
|
||||||
return CancelableWork(sendRelationWork.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun createSendRelationWork(event: Event): OneTimeWorkRequest {
|
|
||||||
//TODO use the new API to send relation (for now use regular send)
|
|
||||||
val sendContentWorkerParams = SendEventWorker.Params(
|
|
||||||
roomId, event)
|
|
||||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
|
||||||
|
|
||||||
return OneTimeWorkRequestBuilder<SendEventWorker>()
|
|
||||||
.setConstraints(WORK_CONSTRAINTS)
|
|
||||||
.setInputData(sendWorkData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {
|
|
||||||
|
|
||||||
val params = FindReactionEventForUndoTask.Params(
|
|
||||||
roomId,
|
|
||||||
targetEventId,
|
|
||||||
reaction,
|
|
||||||
myUserId
|
|
||||||
)
|
|
||||||
findReactionEventForUndoTask.configureWith(params)
|
|
||||||
.enableRetry()
|
|
||||||
.dispatchTo(object : MatrixCallback<FindReactionEventForUndoTask.Result> {
|
|
||||||
override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
|
|
||||||
data.redactEventId?.let { toRedact ->
|
|
||||||
val redactWork = createRedactEventWork(toRedact, null)
|
|
||||||
WorkManager.getInstance()
|
|
||||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork)
|
|
||||||
.enqueue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) {
|
|
||||||
|
|
||||||
val params = UpdateQuickReactionTask.Params(
|
|
||||||
roomId,
|
|
||||||
targetEventId,
|
|
||||||
reaction,
|
|
||||||
oppositeReaction,
|
|
||||||
myUserId
|
|
||||||
)
|
|
||||||
|
|
||||||
updateQuickReactionTask.configureWith(params)
|
|
||||||
.dispatchTo(object : MatrixCallback<UpdateQuickReactionTask.Result> {
|
|
||||||
override fun onSuccess(data: UpdateQuickReactionTask.Result) {
|
|
||||||
data.reactionToAdd?.also { sendReaction(it, targetEventId) }
|
|
||||||
data.reactionToRedact.forEach {
|
|
||||||
val redactWork = createRedactEventWork(it, null)
|
|
||||||
WorkManager.getInstance()
|
|
||||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork)
|
|
||||||
.enqueue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildWorkIdentifier(identifier: String): String {
|
|
||||||
return "${roomId}_$identifier"
|
|
||||||
}
|
|
||||||
|
|
||||||
// private fun saveLocalEcho(event: Event) {
|
|
||||||
// monarchy.tryTransactionAsync { realm ->
|
|
||||||
// val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
|
||||||
// ?: return@tryTransactionAsync
|
|
||||||
// val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId)
|
|
||||||
// ?: return@tryTransactionAsync
|
|
||||||
//
|
|
||||||
// roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
//TODO duplicate with send service?
|
|
||||||
private fun createRedactEventWork(eventId: String, reason: String?): OneTimeWorkRequest {
|
|
||||||
|
|
||||||
//TODO create local echo of m.room.redaction event?
|
|
||||||
|
|
||||||
val sendContentWorkerParams = RedactEventWorker.Params(
|
|
||||||
roomId, eventId, reason)
|
|
||||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
|
||||||
|
|
||||||
return OneTimeWorkRequestBuilder<RedactEventWorker>()
|
|
||||||
.setConstraints(WORK_CONSTRAINTS)
|
|
||||||
.setInputData(redactWorkData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
@ -25,8 +25,12 @@ import im.vector.matrix.android.internal.database.model.EventEntity
|
|||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens to the database for the insertion of any redaction event.
|
||||||
|
* As it will actually delete the content, it should be called last in the list of listener.
|
||||||
|
*/
|
||||||
internal class EventsPruner(monarchy: Monarchy,
|
internal class EventsPruner(monarchy: Monarchy,
|
||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val pruneEventTask: PruneEventTask,
|
private val pruneEventTask: PruneEventTask,
|
||||||
@ -36,6 +40,7 @@ internal class EventsPruner(monarchy: Monarchy,
|
|||||||
override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) }
|
override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) }
|
||||||
|
|
||||||
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||||
|
Timber.v("Event pruner called with ${inserted.size} insertions")
|
||||||
val redactionEvents = inserted
|
val redactionEvents = inserted
|
||||||
.mapNotNull { it.asDomain() }
|
.mapNotNull { it.asDomain() }
|
||||||
|
|
||||||
|
@ -17,14 +17,15 @@ package im.vector.matrix.android.internal.session.room.prune
|
|||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.events.model.*
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
@ -41,8 +42,7 @@ internal interface PruneEventTask : Task<PruneEventTask.Params, Unit> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal class DefaultPruneEventTask(
|
internal class DefaultPruneEventTask(
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy) : PruneEventTask {
|
||||||
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) : PruneEventTask {
|
|
||||||
|
|
||||||
override fun execute(params: PruneEventTask.Params): Try<Unit> {
|
override fun execute(params: PruneEventTask.Params): Try<Unit> {
|
||||||
return monarchy.tryTransactionSync { realm ->
|
return monarchy.tryTransactionSync { realm ->
|
||||||
@ -57,6 +57,12 @@ internal class DefaultPruneEventTask(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId
|
||||||
|
?: "").findFirst()
|
||||||
|
?: return
|
||||||
|
val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT
|
||||||
|
Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")
|
||||||
|
|
||||||
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
|
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
@ -72,19 +78,19 @@ internal class DefaultPruneEventTask(
|
|||||||
?: UnsignedData(null, null)
|
?: UnsignedData(null, null)
|
||||||
|
|
||||||
//was this event a m.replace
|
//was this event a m.replace
|
||||||
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
||||||
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
// if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
||||||
eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
// eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||||
}
|
// }
|
||||||
|
|
||||||
val modified = unsignedData.copy(redactedEvent = redactionEvent)
|
val modified = unsignedData.copy(redactedEvent = redactionEvent)
|
||||||
eventToPrune.content = ContentMapper.map(emptyMap())
|
eventToPrune.content = ContentMapper.map(emptyMap())
|
||||||
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
|
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
|
||||||
|
|
||||||
}
|
}
|
||||||
EventType.REACTION -> {
|
// EventType.REACTION -> {
|
||||||
eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
|
// eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.session.room.relation
|
||||||
|
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
|
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||||
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
||||||
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
|
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
|
||||||
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
|
import im.vector.matrix.android.internal.util.CancelableWork
|
||||||
|
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||||
|
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
: RelationService {
|
||||||
|
|
||||||
|
|
||||||
|
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
|
||||||
|
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
||||||
|
.also {
|
||||||
|
saveLocalEcho(it)
|
||||||
|
}
|
||||||
|
val sendRelationWork = createSendRelationWork(event)
|
||||||
|
TimelineSendEventWorkCommon.postWork(roomId, sendRelationWork)
|
||||||
|
return CancelableWork(sendRelationWork.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun createSendRelationWork(event: Event): OneTimeWorkRequest {
|
||||||
|
val sendContentWorkerParams = SendEventWorker.Params(
|
||||||
|
roomId, event)
|
||||||
|
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
|
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {
|
||||||
|
|
||||||
|
val params = FindReactionEventForUndoTask.Params(
|
||||||
|
roomId,
|
||||||
|
targetEventId,
|
||||||
|
reaction,
|
||||||
|
myUserId
|
||||||
|
)
|
||||||
|
findReactionEventForUndoTask.configureWith(params)
|
||||||
|
.enableRetry()
|
||||||
|
.dispatchTo(object : MatrixCallback<FindReactionEventForUndoTask.Result> {
|
||||||
|
override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
|
||||||
|
if (data.redactEventId == null) {
|
||||||
|
Timber.w("Cannot find reaction to undo (not yet synced?)")
|
||||||
|
//TODO?
|
||||||
|
}
|
||||||
|
data.redactEventId?.let { toRedact ->
|
||||||
|
|
||||||
|
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
|
||||||
|
saveLocalEcho(it)
|
||||||
|
}
|
||||||
|
val redactWork = createRedactEventWork(redactEvent, toRedact, null)
|
||||||
|
|
||||||
|
TimelineSendEventWorkCommon.postWork(roomId, redactWork)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) {
|
||||||
|
|
||||||
|
val params = UpdateQuickReactionTask.Params(
|
||||||
|
roomId,
|
||||||
|
targetEventId,
|
||||||
|
reaction,
|
||||||
|
oppositeReaction,
|
||||||
|
myUserId
|
||||||
|
)
|
||||||
|
|
||||||
|
updateQuickReactionTask.configureWith(params)
|
||||||
|
.dispatchTo(object : MatrixCallback<UpdateQuickReactionTask.Result> {
|
||||||
|
override fun onSuccess(data: UpdateQuickReactionTask.Result) {
|
||||||
|
data.reactionToAdd?.also { sendReaction(it, targetEventId) }
|
||||||
|
data.reactionToRedact.forEach {
|
||||||
|
val redactEvent = eventFactory.createRedactEvent(roomId, it, null).also {
|
||||||
|
saveLocalEcho(it)
|
||||||
|
}
|
||||||
|
val redactWork = createRedactEventWork(redactEvent, it, null)
|
||||||
|
TimelineSendEventWorkCommon.postWork(roomId, redactWork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildWorkIdentifier(identifier: String): String {
|
||||||
|
return "${roomId}_$identifier"
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO duplicate with send service?
|
||||||
|
private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest {
|
||||||
|
|
||||||
|
val sendContentWorkerParams = RedactEventWorker.Params(localEvent.eventId!!,
|
||||||
|
roomId, eventId, reason)
|
||||||
|
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable {
|
||||||
|
val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText).also {
|
||||||
|
saveLocalEcho(it)
|
||||||
|
}
|
||||||
|
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||||
|
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
|
//TODO use relation API?
|
||||||
|
|
||||||
|
val workRequest = TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||||
|
TimelineSendEventWorkCommon.postWork(roomId, workRequest)
|
||||||
|
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 = TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||||
|
TimelineSendEventWorkCommon.postWork(roomId, workRequest)
|
||||||
|
return CancelableWork(workRequest.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the event in database as a local echo.
|
||||||
|
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.
|
||||||
|
* The sendingTimelineEvents is checked on new sync and will remove the local echo if an event with
|
||||||
|
* the same transaction id is received (in unsigned data)
|
||||||
|
*/
|
||||||
|
private fun saveLocalEcho(event: Event) {
|
||||||
|
monarchy.tryTransactionAsync { realm ->
|
||||||
|
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||||
|
?: return@tryTransactionAsync
|
||||||
|
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId)
|
||||||
|
?: return@tryTransactionAsync
|
||||||
|
|
||||||
|
roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 arrow.core.Try
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 android.content.Context
|
||||||
import androidx.work.Worker
|
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.failure.Failure
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.events.model.toModel
|
||||||
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.annotation.ReactionInfo
|
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.di.MatrixKoinComponent
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
@ -70,7 +70,11 @@ class SendRelationWorker(context: Context, params: WorkerParameters)
|
|||||||
return result.fold({
|
return result.fold({
|
||||||
when (it) {
|
when (it) {
|
||||||
is Failure.NetworkConnection -> Result.retry()
|
is Failure.NetworkConnection -> Result.retry()
|
||||||
else -> Result.failure()
|
else -> {
|
||||||
|
//TODO mark as failed to send?
|
||||||
|
//always return success, or the chain will be stuck for ever!
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, { Result.success() })
|
}, { Result.success() })
|
||||||
}
|
}
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 arrow.core.Try
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
@ -24,18 +24,12 @@ import im.vector.matrix.android.api.session.room.send.SendService
|
|||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.api.util.CancelableBag
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
import im.vector.matrix.android.api.util.addTo
|
import im.vector.matrix.android.api.util.addTo
|
||||||
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
||||||
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||||
import im.vector.matrix.android.internal.util.CancelableWork
|
import im.vector.matrix.android.internal.util.CancelableWork
|
||||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private const val SEND_WORK = "SEND_WORK"
|
|
||||||
private const val UPLOAD_WORK = "UPLOAD_WORK"
|
private const val UPLOAD_WORK = "UPLOAD_WORK"
|
||||||
private const val BACKOFF_DELAY = 10_000L
|
private const val BACKOFF_DELAY = 10_000L
|
||||||
|
|
||||||
@ -49,14 +43,21 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
: SendService {
|
: SendService {
|
||||||
|
|
||||||
|
|
||||||
override fun sendTextMessage(text: String, msgType: String): Cancelable {
|
override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
|
||||||
val event = eventFactory.createTextEvent(roomId, msgType, text).also {
|
val event = eventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
val sendWork = createSendEventWork(event)
|
val sendWork = createSendEventWork(event)
|
||||||
WorkManager.getInstance()
|
TimelineSendEventWorkCommon.postWork(roomId, sendWork)
|
||||||
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork)
|
return CancelableWork(sendWork.id)
|
||||||
.enqueue()
|
}
|
||||||
|
|
||||||
|
override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable {
|
||||||
|
val event = eventFactory.createFormattedTextEvent(roomId, text, formattedText).also {
|
||||||
|
saveLocalEcho(it)
|
||||||
|
}
|
||||||
|
val sendWork = createSendEventWork(event)
|
||||||
|
TimelineSendEventWorkCommon.postWork(roomId, sendWork)
|
||||||
return CancelableWork(sendWork.id)
|
return CancelableWork(sendWork.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,12 +70,9 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun redactEvent(event: Event, reason: String?): Cancelable {
|
override fun redactEvent(event: Event, reason: String?): Cancelable {
|
||||||
//TODO manage local echo ?
|
|
||||||
//TODO manage media/attachements?
|
//TODO manage media/attachements?
|
||||||
val redactWork = createRedactEventWork(event, reason)
|
val redactWork = createRedactEventWork(event, reason)
|
||||||
WorkManager.getInstance()
|
TimelineSendEventWorkCommon.postWork(roomId, redactWork)
|
||||||
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, redactWork)
|
|
||||||
.enqueue()
|
|
||||||
return CancelableWork(redactWork.id)
|
return CancelableWork(redactWork.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,14 +93,7 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveLocalEcho(event: Event) {
|
private fun saveLocalEcho(event: Event) {
|
||||||
monarchy.tryTransactionAsync { realm ->
|
eventFactory.saveLocalEcho(monarchy, event)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildWorkIdentifier(identifier: String): String {
|
private fun buildWorkIdentifier(identifier: String): String {
|
||||||
@ -113,26 +104,20 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
return OneTimeWorkRequestBuilder<SendEventWorker>()
|
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||||
.setConstraints(WORK_CONSTRAINTS)
|
|
||||||
.setInputData(sendWorkData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
||||||
|
|
||||||
//TODO create local echo of m.room.redaction event?
|
val redactEvent = eventFactory.createRedactEvent(roomId, event.eventId!!, reason).also {
|
||||||
|
saveLocalEcho(it)
|
||||||
|
}
|
||||||
|
|
||||||
val sendContentWorkerParams = RedactEventWorker.Params(
|
val sendContentWorkerParams = RedactEventWorker.Params(redactEvent.eventId!!,
|
||||||
roomId, event.eventId!!, reason)
|
roomId, event.eventId, reason)
|
||||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
return OneTimeWorkRequestBuilder<RedactEventWorker>()
|
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
||||||
.setConstraints(WORK_CONSTRAINTS)
|
|
||||||
.setInputData(redactWorkData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest {
|
private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest {
|
||||||
|
@ -17,24 +17,98 @@
|
|||||||
package im.vector.matrix.android.internal.session.room.send
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.R
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.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.content.ContentAttachmentData
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.*
|
||||||
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.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
|
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
|
||||||
|
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
|
||||||
|
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
||||||
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
||||||
|
import im.vector.matrix.android.internal.util.StringProvider
|
||||||
|
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||||
|
import org.commonmark.parser.Parser
|
||||||
|
import org.commonmark.renderer.html.HtmlRenderer
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
internal class LocalEchoEventFactory(private val credentials: Credentials) {
|
/**
|
||||||
|
* Creates local echo of events for room events.
|
||||||
|
* A local echo is an event that is persisted even if not yet sent to the server,
|
||||||
|
* in an optimistic way (as if the server as responded immediately). Local echo are using a local id,
|
||||||
|
* (the transaction ID), this id is used when receiving an event from a sync to check if this event
|
||||||
|
* is matching an existing local echo.
|
||||||
|
*
|
||||||
|
* The transactionID is used as loc
|
||||||
|
*/
|
||||||
|
internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
fun createTextEvent(roomId: String, msgType: String, text: String): 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 (isFormattedTextPertinent(text, htmlText)) { //FIXME
|
||||||
|
return createFormattedTextEvent(roomId, text, htmlText)
|
||||||
|
}
|
||||||
|
}
|
||||||
val content = MessageTextContent(type = msgType, body = text)
|
val content = MessageTextContent(type = msgType, body = text)
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isFormattedTextPertinent(text: String, htmlText: String?) =
|
||||||
|
text != htmlText && htmlText != "<p>$text</p>\n"
|
||||||
|
|
||||||
|
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 (isFormattedTextPertinent(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 {
|
fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||||
return when (attachment.type) {
|
return when (attachment.type) {
|
||||||
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
|
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
|
||||||
@ -52,14 +126,16 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) {
|
|||||||
reaction
|
reaction
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
val localId = dummyEventId(roomId)
|
||||||
return Event(
|
return Event(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
originServerTs = dummyOriginServerTs(),
|
originServerTs = dummyOriginServerTs(),
|
||||||
sender = credentials.userId,
|
sender = credentials.userId,
|
||||||
eventId = dummyEventId(roomId),
|
eventId = localId,
|
||||||
type = EventType.REACTION,
|
type = EventType.REACTION,
|
||||||
content = content.toContent()
|
content = content.toContent(),
|
||||||
)
|
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -141,13 +217,15 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createEvent(roomId: String, content: Any? = null): Event {
|
private fun createEvent(roomId: String, content: Any? = null): Event {
|
||||||
|
val localID = dummyEventId(roomId)
|
||||||
return Event(
|
return Event(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
originServerTs = dummyOriginServerTs(),
|
originServerTs = dummyOriginServerTs(),
|
||||||
sender = credentials.userId,
|
sender = credentials.userId,
|
||||||
eventId = dummyEventId(roomId),
|
eventId = localID,
|
||||||
type = EventType.MESSAGE,
|
type = EventType.MESSAGE,
|
||||||
content = content.toContent()
|
content = content.toContent(),
|
||||||
|
unsignedData = UnsignedData(age = null, transactionId = localID)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,6 +234,120 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun dummyEventId(roomId: String): String {
|
private fun dummyEventId(roomId: String): String {
|
||||||
return roomId + "-" + dummyOriginServerTs()
|
return "m.${UUID.randomUUID()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? {
|
||||||
|
//Fallbacks and event representation
|
||||||
|
//TODO Add error/warning logs when any of this is null
|
||||||
|
val permalink = PermalinkFactory.createPermalink(eventReplied) ?: return null
|
||||||
|
val userId = eventReplied.sender ?: return null
|
||||||
|
val userLink = PermalinkFactory.createPermalink(userId) ?: return null
|
||||||
|
// <mx-reply>
|
||||||
|
// <blockquote>
|
||||||
|
// <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a>
|
||||||
|
// <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
|
||||||
|
// <br />
|
||||||
|
// <!-- This is where the related event's HTML would be. -->
|
||||||
|
// </blockquote>
|
||||||
|
// </mx-reply>
|
||||||
|
// This is where the reply goes.
|
||||||
|
val body = bodyForReply(eventReplied.content.toModel<MessageContent>())
|
||||||
|
val replyFallbackTemplateFormatted = """
|
||||||
|
<mx-reply><blockquote><a href="%s">${stringProvider.getString(R.string.message_reply_to_prefix)}</a><a href="%s">%s</a><br />%s</blockquote></mx-reply>%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 <Plain Text, Formatted Text?> used for the fallback event representation
|
||||||
|
* in a reply message.
|
||||||
|
*/
|
||||||
|
private fun bodyForReply(content: MessageContent?): Pair<String, String?> {
|
||||||
|
when (content?.type) {
|
||||||
|
MessageType.MSGTYPE_EMOTE,
|
||||||
|
MessageType.MSGTYPE_TEXT,
|
||||||
|
MessageType.MSGTYPE_NOTICE -> {
|
||||||
|
//If we already have formatted body, return it?
|
||||||
|
var formattedText: String? = null
|
||||||
|
if (content is MessageTextContent) {
|
||||||
|
if (content.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||||
|
formattedText = content.formattedBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content.body to formattedText
|
||||||
|
}
|
||||||
|
MessageType.MSGTYPE_FILE -> return stringProvider.getString(R.string.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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* {
|
||||||
|
"content": {
|
||||||
|
"reason": "Spamming"
|
||||||
|
},
|
||||||
|
"event_id": "$143273582443PhrSn:domain.com",
|
||||||
|
"origin_server_ts": 1432735824653,
|
||||||
|
"redacts": "$fukweghifu23:localhost",
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:domain.com",
|
||||||
|
"sender": "@example:domain.com",
|
||||||
|
"type": "m.room.redaction",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event {
|
||||||
|
val localID = dummyEventId(roomId)
|
||||||
|
return Event(
|
||||||
|
roomId = roomId,
|
||||||
|
originServerTs = dummyOriginServerTs(),
|
||||||
|
sender = credentials.userId,
|
||||||
|
eventId = localID,
|
||||||
|
type = EventType.REDACTION,
|
||||||
|
redacts = eventId,
|
||||||
|
content = reason?.let { mapOf("reason" to it).toContent() },
|
||||||
|
unsignedData = UnsignedData(age = null, transactionId = localID)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLocalEcho(monarchy: Monarchy, event: Event) {
|
||||||
|
monarchy.tryTransactionAsync { realm ->
|
||||||
|
val roomEntity = RoomEntity.where(realm, roomId = event.roomId!!).findFirst()
|
||||||
|
?: return@tryTransactionAsync
|
||||||
|
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = event.roomId)
|
||||||
|
?: return@tryTransactionAsync
|
||||||
|
|
||||||
|
roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,13 +25,13 @@ import im.vector.matrix.android.internal.network.executeRequest
|
|||||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||||
import org.koin.standalone.inject
|
import org.koin.standalone.inject
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
internal class RedactEventWorker(context: Context, params: WorkerParameters)
|
internal class RedactEventWorker(context: Context, params: WorkerParameters)
|
||||||
: Worker(context, params), MatrixKoinComponent {
|
: Worker(context, params), MatrixKoinComponent {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class Params(
|
internal data class Params(
|
||||||
|
val txID: String,
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
val reason: String?
|
val reason: String?
|
||||||
@ -40,26 +40,26 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters)
|
|||||||
private val roomAPI by inject<RoomAPI>()
|
private val roomAPI by inject<RoomAPI>()
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<RedactEventWorker.Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
|
||||||
if (params.eventId == null) {
|
val eventId = params.eventId
|
||||||
return Result.failure()
|
|
||||||
}
|
|
||||||
val txID = UUID.randomUUID().toString()
|
|
||||||
|
|
||||||
val result = executeRequest<SendResponse> {
|
val result = executeRequest<SendResponse> {
|
||||||
apiCall = roomAPI.redactEvent(
|
apiCall = roomAPI.redactEvent(
|
||||||
txID,
|
params.txID,
|
||||||
params.roomId,
|
params.roomId,
|
||||||
params.eventId,
|
eventId,
|
||||||
if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
|
if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return result.fold({
|
return result.fold({
|
||||||
when (it) {
|
when (it) {
|
||||||
is Failure.NetworkConnection -> Result.retry()
|
is Failure.NetworkConnection -> Result.retry()
|
||||||
else -> Result.failure()
|
else -> {
|
||||||
|
//TODO mark as failed to send?
|
||||||
|
//always return success, or the chain will be stuck for ever!
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
Result.success()
|
Result.success()
|
||||||
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
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.Event
|
||||||
import im.vector.matrix.android.internal.di.MatrixKoinComponent
|
import im.vector.matrix.android.internal.di.MatrixKoinComponent
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
@ -57,6 +58,15 @@ internal class SendEventWorker(context: Context, params: WorkerParameters)
|
|||||||
localEvent.content
|
localEvent.content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return result.fold({ Result.retry() }, { Result.success() })
|
return result.fold({
|
||||||
|
when (it) {
|
||||||
|
is Failure.NetworkConnection -> Result.retry()
|
||||||
|
else -> {
|
||||||
|
//TODO mark as failed to send?
|
||||||
|
//always return success, or the chain will be stuck for ever!
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { Result.success() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.session.room.timeline
|
||||||
|
|
||||||
|
import androidx.work.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
|
private const val SEND_WORK = "SEND_WORK"
|
||||||
|
private const val BACKOFF_DELAY = 10_000L
|
||||||
|
|
||||||
|
private val WORK_CONSTRAINTS = Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for sending event related works.
|
||||||
|
* All send event from a room are using the same workchain, in order to ensure order.
|
||||||
|
* WorkRequest must always return success (even if server error, in this case marking the event as failed to send)
|
||||||
|
* , if not the chain will be doomed in failed state.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
internal object TimelineSendEventWorkCommon {
|
||||||
|
|
||||||
|
fun postWork(roomId: String, workRequest: OneTimeWorkRequest) {
|
||||||
|
WorkManager.getInstance()
|
||||||
|
.beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, workRequest)
|
||||||
|
.enqueue()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified W : ListenableWorker> createWork(data: Data): OneTimeWorkRequest {
|
||||||
|
return OneTimeWorkRequestBuilder<W>()
|
||||||
|
.setConstraints(WORK_CONSTRAINTS)
|
||||||
|
.setInputData(data)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildWorkIdentifier(roomId: String): String {
|
||||||
|
return "${roomId}_$SEND_WORK"
|
||||||
|
}
|
||||||
|
}
|
@ -18,20 +18,13 @@ package im.vector.matrix.android.internal.session.room.timeline
|
|||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.internal.database.helper.addAll
|
import im.vector.matrix.android.internal.database.helper.*
|
||||||
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
|
||||||
import im.vector.matrix.android.internal.database.helper.addStateEvents
|
|
||||||
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
|
||||||
import im.vector.matrix.android.internal.database.helper.isUnlinked
|
|
||||||
import im.vector.matrix.android.internal.database.helper.merge
|
|
||||||
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
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.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.query.create
|
import im.vector.matrix.android.internal.database.query.create
|
||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -39,8 +32,7 @@ import timber.log.Timber
|
|||||||
/**
|
/**
|
||||||
* Insert Chunk in DB, and eventually merge with existing chunk event
|
* Insert Chunk in DB, and eventually merge with existing chunk event
|
||||||
*/
|
*/
|
||||||
internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
|
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
||||||
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
* <pre>
|
||||||
@ -151,8 +143,6 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
|
|||||||
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
||||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
||||||
|
|
||||||
//Event
|
|
||||||
eventRelationsAggregationUpdater.update(realm,roomId,receivedChunk.events.toList())
|
|
||||||
// Then we merge chunks if needed
|
// Then we merge chunks if needed
|
||||||
if (currentChunk != prevChunk && prevChunk != null) {
|
if (currentChunk != prevChunk && prevChunk != null) {
|
||||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
||||||
|
@ -31,14 +31,9 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
|
|||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
|
||||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||||
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
import im.vector.matrix.android.internal.session.sync.model.*
|
||||||
import im.vector.matrix.android.internal.session.sync.model.RoomSync
|
|
||||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
|
|
||||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
|
|
||||||
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
|
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -46,8 +41,7 @@ import timber.log.Timber
|
|||||||
internal class RoomSyncHandler(private val monarchy: Monarchy,
|
internal class RoomSyncHandler(private val monarchy: Monarchy,
|
||||||
private val readReceiptHandler: ReadReceiptHandler,
|
private val readReceiptHandler: ReadReceiptHandler,
|
||||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||||
private val roomTagHandler: RoomTagHandler,
|
private val roomTagHandler: RoomTagHandler) {
|
||||||
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
|
|
||||||
|
|
||||||
sealed class HandlingStrategy {
|
sealed class HandlingStrategy {
|
||||||
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
|
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
|
||||||
@ -116,11 +110,13 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||||||
transactionIds.forEach {
|
transactionIds.forEach {
|
||||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||||
if (sendingEventEntity != null) {
|
if (sendingEventEntity != null) {
|
||||||
|
Timber.v("Remove local echo for tx:$it")
|
||||||
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
||||||
|
} else {
|
||||||
|
Timber.v("Can't find corresponding local echo for tx:$it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
eventRelationsAggregationUpdater.update(realm, roomId, roomSync.timeline?.events)
|
|
||||||
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
|
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
|
||||||
|
|
||||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
||||||
|
@ -40,7 +40,7 @@ internal class SyncModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
RoomSyncHandler(get(), get(), get(), get(), get())
|
RoomSyncHandler(get(), get(), get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -88,7 +88,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected fun Disposable.disposeOnDestroy(): Disposable {
|
protected fun Disposable.disposeOnDestroy(): Disposable {
|
||||||
// TODO Ganfra: never disposed...
|
|
||||||
uiDisposables.add(this)
|
uiDisposables.add(this)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@ -128,6 +127,8 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
|||||||
|
|
||||||
unBinder?.unbind()
|
unBinder?.unbind()
|
||||||
unBinder = null
|
unBinder = null
|
||||||
|
|
||||||
|
uiDisposables.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -20,11 +20,13 @@ import android.text.Spannable
|
|||||||
import com.otaliastudios.autocomplete.AutocompletePolicy
|
import com.otaliastudios.autocomplete.AutocompletePolicy
|
||||||
|
|
||||||
class CommandAutocompletePolicy : AutocompletePolicy {
|
class CommandAutocompletePolicy : AutocompletePolicy {
|
||||||
|
|
||||||
|
var enabled: Boolean = true
|
||||||
|
|
||||||
override fun getQuery(text: Spannable): CharSequence {
|
override fun getQuery(text: Spannable): CharSequence {
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
return text.substring(1, text.length)
|
return text.substring(1, text.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should not happen
|
// Should not happen
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -34,7 +36,7 @@ class CommandAutocompletePolicy : AutocompletePolicy {
|
|||||||
|
|
||||||
// Only if text which starts with '/' and without space
|
// Only if text which starts with '/' and without space
|
||||||
override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean {
|
override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean {
|
||||||
return text?.startsWith("/") == true
|
return enabled && text?.startsWith("/") == true
|
||||||
&& !text.contains(" ")
|
&& !text.contains(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,9 +72,10 @@ class HomeModule {
|
|||||||
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
|
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
|
||||||
val colorProvider = ColorProvider(fragment.requireContext())
|
val colorProvider = ColorProvider(fragment.requireContext())
|
||||||
val timelineDateFormatter = get<TimelineDateFormatter>()
|
val timelineDateFormatter = get<TimelineDateFormatter>()
|
||||||
|
val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer, get())
|
||||||
|
|
||||||
val timelineItemFactory = TimelineItemFactory(
|
val timelineItemFactory = TimelineItemFactory(
|
||||||
messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer),
|
messageItemFactory = messageItemFactory,
|
||||||
noticeItemFactory = NoticeItemFactory(noticeEventFormatter),
|
noticeItemFactory = NoticeItemFactory(noticeEventFormatter),
|
||||||
defaultItemFactory = DefaultItemFactory()
|
defaultItemFactory = DefaultItemFactory()
|
||||||
)
|
)
|
||||||
|
@ -23,17 +23,21 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|||||||
|
|
||||||
sealed class RoomDetailActions {
|
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<MediaFile>) : RoomDetailActions()
|
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
|
||||||
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||||
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
||||||
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
|
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
|
||||||
data class RedactAction(val targetEventId: String, val reason: 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 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 UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val opposite: String) : RoomDetailActions()
|
||||||
data class ShowEditHistoryAction(val event: String,val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
|
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
|
||||||
object AcceptInvite : RoomDetailActions()
|
object AcceptInvite : RoomDetailActions()
|
||||||
object RejectInvite : 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()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -40,6 +40,7 @@ import androidx.lifecycle.Observer
|
|||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import butterknife.BindView
|
||||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
@ -53,6 +54,7 @@ import com.otaliastudios.autocomplete.Autocomplete
|
|||||||
import com.otaliastudios.autocomplete.AutocompleteCallback
|
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||||
import com.otaliastudios.autocomplete.CharPolicy
|
import com.otaliastudios.autocomplete.CharPolicy
|
||||||
import im.vector.matrix.android.api.session.Session
|
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.EditAggregatedSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
@ -74,6 +76,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer
|
|||||||
import im.vector.riotredesign.features.home.HomeModule
|
import im.vector.riotredesign.features.home.HomeModule
|
||||||
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
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.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.TextComposerViewModel
|
||||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
|
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||||
@ -89,12 +92,17 @@ import im.vector.riotredesign.features.media.ImageMediaViewerActivity
|
|||||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||||
import im.vector.riotredesign.features.media.VideoMediaViewerActivity
|
import im.vector.riotredesign.features.media.VideoMediaViewerActivity
|
||||||
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
|
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
|
||||||
|
import im.vector.riotredesign.features.settings.PreferencesManager
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
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.ext.android.inject
|
||||||
import org.koin.android.scope.ext.android.bindScope
|
import org.koin.android.scope.ext.android.bindScope
|
||||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
import org.koin.android.scope.ext.android.getOrCreateScope
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
import ru.noties.markwon.Markwon
|
||||||
|
import ru.noties.markwon.html.HtmlPlugin
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -132,13 +140,12 @@ class RoomDetailFragment :
|
|||||||
* @return the sanitized display name
|
* @return the sanitized display name
|
||||||
*/
|
*/
|
||||||
fun sanitizeDisplayname(displayName: String): String? {
|
fun sanitizeDisplayname(displayName: String): String? {
|
||||||
var displayName = displayName
|
|
||||||
// sanity checks
|
// sanity checks
|
||||||
if (!TextUtils.isEmpty(displayName)) {
|
if (!TextUtils.isEmpty(displayName)) {
|
||||||
val ircPattern = " (IRC)"
|
val ircPattern = " (IRC)"
|
||||||
|
|
||||||
if (displayName.endsWith(ircPattern)) {
|
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 roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||||
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
||||||
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
|
private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
|
||||||
|
private val commandAutocompletePolicy = CommandAutocompletePolicy()
|
||||||
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) }
|
private val autocompleteCommandPresenter: AutocompleteCommandPresenter by inject { parametersOf(this) }
|
||||||
private val autocompleteUserPresenter: AutocompleteUserPresenter by inject { parametersOf(this) }
|
private val autocompleteUserPresenter: AutocompleteUserPresenter by inject { parametersOf(this) }
|
||||||
private val homePermalinkHandler: HomePermalinkHandler by inject()
|
private val homePermalinkHandler: HomePermalinkHandler by inject()
|
||||||
@ -165,6 +173,9 @@ class RoomDetailFragment :
|
|||||||
|
|
||||||
private lateinit var actionViewModel: ActionsHandler
|
private lateinit var actionViewModel: ActionsHandler
|
||||||
|
|
||||||
|
@BindView(R.id.composerLayout)
|
||||||
|
lateinit var composerLayout: TextComposerView
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||||
@ -187,6 +198,77 @@ class RoomDetailFragment :
|
|||||||
actionViewModel.actionCommandEvent.observe(this, Observer {
|
actionViewModel.actionCommandEvent.observe(this, Observer {
|
||||||
handleActions(it)
|
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?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@ -233,8 +315,8 @@ class RoomDetailFragment :
|
|||||||
private fun setupComposer() {
|
private fun setupComposer() {
|
||||||
val elevation = 6f
|
val elevation = 6f
|
||||||
val backgroundDrawable = ColorDrawable(Color.WHITE)
|
val backgroundDrawable = ColorDrawable(Color.WHITE)
|
||||||
Autocomplete.on<Command>(composerEditText)
|
Autocomplete.on<Command>(composerLayout.composerEditText)
|
||||||
.with(CommandAutocompletePolicy())
|
.with(commandAutocompletePolicy)
|
||||||
.with(autocompleteCommandPresenter)
|
.with(autocompleteCommandPresenter)
|
||||||
.with(elevation)
|
.with(elevation)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
@ -253,7 +335,7 @@ class RoomDetailFragment :
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
autocompleteUserPresenter.callback = this
|
autocompleteUserPresenter.callback = this
|
||||||
Autocomplete.on<User>(composerEditText)
|
Autocomplete.on<User>(composerLayout.composerEditText)
|
||||||
.with(CharPolicy('@', true))
|
.with(CharPolicy('@', true))
|
||||||
.with(autocompleteUserPresenter)
|
.with(autocompleteUserPresenter)
|
||||||
.with(elevation)
|
.with(elevation)
|
||||||
@ -281,7 +363,7 @@ class RoomDetailFragment :
|
|||||||
// Add the span
|
// Add the span
|
||||||
val user = session.getUser(item.userId)
|
val user = session.getUser(item.userId)
|
||||||
val span = PillImageSpan(glideRequests, context!!, item.userId, user)
|
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)
|
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
|
||||||
@ -293,16 +375,16 @@ class RoomDetailFragment :
|
|||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
sendButton.setOnClickListener {
|
composerLayout.sendButton.setOnClickListener {
|
||||||
val textMessage = composerEditText.text.toString()
|
val textMessage = composerLayout.composerEditText.text.toString()
|
||||||
if (textMessage.isNotBlank()) {
|
if (textMessage.isNotBlank()) {
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
|
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage, PreferencesManager.isMarkdownEnabled(requireContext())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAttachmentButton() {
|
private fun setupAttachmentButton() {
|
||||||
attachmentButton.setOnClickListener {
|
composerLayout.attachmentButton.setOnClickListener {
|
||||||
val intent = Intent(requireContext(), FilePickerActivity::class.java)
|
val intent = Intent(requireContext(), FilePickerActivity::class.java)
|
||||||
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
|
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
|
||||||
.setCheckPermission(true)
|
.setCheckPermission(true)
|
||||||
@ -386,6 +468,11 @@ class RoomDetailFragment :
|
|||||||
if (summary?.membership == Membership.JOIN) {
|
if (summary?.membership == Membership.JOIN) {
|
||||||
timelineEventController.setTimeline(state.timeline)
|
timelineEventController.setTimeline(state.timeline)
|
||||||
inviteView.visibility = View.GONE
|
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) {
|
} else if (summary?.membership == Membership.INVITE && inviter != null) {
|
||||||
inviteView.visibility = View.VISIBLE
|
inviteView.visibility = View.VISIBLE
|
||||||
inviteView.render(inviter, VectorInviteView.Mode.LARGE)
|
inviteView.render(inviter, VectorInviteView.Mode.LARGE)
|
||||||
@ -416,7 +503,7 @@ class RoomDetailFragment :
|
|||||||
is SendMessageResult.MessageSent,
|
is SendMessageResult.MessageSent,
|
||||||
is SendMessageResult.SlashCommandHandled -> {
|
is SendMessageResult.SlashCommandHandled -> {
|
||||||
// Clear composer
|
// Clear composer
|
||||||
composerEditText.text = null
|
composerLayout.composerEditText.text = null
|
||||||
}
|
}
|
||||||
is SendMessageResult.SlashCommandError -> {
|
is SendMessageResult.SlashCommandError -> {
|
||||||
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
|
||||||
@ -586,6 +673,18 @@ class RoomDetailFragment :
|
|||||||
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite))
|
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 -> {
|
else -> {
|
||||||
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
@ -599,6 +698,7 @@ class RoomDetailFragment :
|
|||||||
*
|
*
|
||||||
* @param text the text to insert.
|
* @param text the text to insert.
|
||||||
*/
|
*/
|
||||||
|
//TODO legacy, refactor
|
||||||
private fun insertUserDisplayNameInTextEditor(text: String?) {
|
private fun insertUserDisplayNameInTextEditor(text: String?) {
|
||||||
//TODO move logic outside of fragment
|
//TODO move logic outside of fragment
|
||||||
if (null != text) {
|
if (null != text) {
|
||||||
@ -607,21 +707,21 @@ class RoomDetailFragment :
|
|||||||
val myDisplayName = session.getUser(session.sessionParams.credentials.userId)?.displayName
|
val myDisplayName = session.getUser(session.sessionParams.credentials.userId)?.displayName
|
||||||
if (TextUtils.equals(myDisplayName, text)) {
|
if (TextUtils.equals(myDisplayName, text)) {
|
||||||
// current user
|
// current user
|
||||||
if (TextUtils.isEmpty(composerEditText.text)) {
|
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
|
||||||
composerEditText.append(Command.EMOTE.command + " ")
|
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
|
||||||
composerEditText.setSelection(composerEditText.text.length)
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||||
// vibrate = true
|
// vibrate = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// another user
|
// another user
|
||||||
if (TextUtils.isEmpty(composerEditText.text)) {
|
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
|
||||||
// Ensure displayName will not be interpreted as a Slash command
|
// Ensure displayName will not be interpreted as a Slash command
|
||||||
if (text.startsWith("/")) {
|
if (text.startsWith("/")) {
|
||||||
composerEditText.append("\\")
|
composerLayout.composerEditText.append("\\")
|
||||||
}
|
}
|
||||||
composerEditText.append(sanitizeDisplayname(text)!! + ": ")
|
composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ")
|
||||||
} else {
|
} else {
|
||||||
composerEditText.text.insert(composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
|
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// vibrate = true
|
// vibrate = true
|
||||||
@ -633,12 +733,16 @@ class RoomDetailFragment :
|
|||||||
// v.vibrate(100)
|
// v.vibrate(100)
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
composerEditText.requestFocus()
|
focusComposerAndShowKeyboard()
|
||||||
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
||||||
imm?.showSoftInput(composerEditText, InputMethodManager.SHOW_FORCED)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
|
||||||
val snack = Snackbar.make(view!!, message, duration)
|
val snack = Snackbar.make(view!!, message, duration)
|
||||||
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
|
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
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.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
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.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.model.message.MessageType
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.platform.VectorViewModel
|
import im.vector.riotredesign.core.platform.VectorViewModel
|
||||||
@ -35,11 +39,14 @@ import im.vector.riotredesign.features.command.CommandParser
|
|||||||
import im.vector.riotredesign.features.command.ParsedCommand
|
import im.vector.riotredesign.features.command.ParsedCommand
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
|
import org.commonmark.parser.Parser
|
||||||
|
import org.commonmark.renderer.html.HtmlRenderer
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
class RoomDetailViewModel(initialState: RoomDetailViewState,
|
class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||||
private val session: Session
|
private val session: Session
|
||||||
) : VectorViewModel<RoomDetailViewState>(initialState) {
|
) : VectorViewModel<RoomDetailViewState>(initialState) {
|
||||||
@ -83,9 +90,29 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
|
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
|
||||||
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||||
is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(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<LiveEvent<Pair<Int, List<Any>>>>()
|
private val _nonBlockingPopAlert = MutableLiveData<LiveEvent<Pair<Int, List<Any>>>>()
|
||||||
val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
|
val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
|
||||||
@ -99,13 +126,15 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
|
||||||
// Handle slash command
|
withState { state ->
|
||||||
|
when (state.sendMode) {
|
||||||
|
SendMode.REGULAR -> {
|
||||||
val slashCommandResult = CommandParser.parseSplashCommand(action.text)
|
val slashCommandResult = CommandParser.parseSplashCommand(action.text)
|
||||||
|
|
||||||
when (slashCommandResult) {
|
when (slashCommandResult) {
|
||||||
is ParsedCommand.ErrorNotACommand -> {
|
is ParsedCommand.ErrorNotACommand -> {
|
||||||
// Send the text message to the room
|
// Send the text message to the room
|
||||||
room.sendTextMessage(action.text)
|
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
|
||||||
}
|
}
|
||||||
is ParsedCommand.ErrorSyntax -> {
|
is ParsedCommand.ErrorSyntax -> {
|
||||||
@ -165,6 +194,78 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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) {
|
private fun handleShowEditHistoryReaction(action: RoomDetailActions.ShowEditHistoryAction) {
|
||||||
//TODO temporary implementation
|
//TODO temporary implementation
|
||||||
@ -263,6 +364,34 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
room.join(object : MatrixCallback<Unit> {})
|
room.join(object : MatrixCallback<Unit> {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private fun observeEventDisplayedActions() {
|
||||||
// We are buffering scroll events for one second
|
// We are buffering scroll events for one second
|
||||||
|
@ -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.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
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.TimelineData
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
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(
|
data class RoomDetailViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String?,
|
val eventId: String?,
|
||||||
val timeline: Timeline? = null,
|
val timeline: Timeline? = null,
|
||||||
val asyncInviter: Async<User> = Uninitialized,
|
val asyncInviter: Async<User> = Uninitialized,
|
||||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||||
val asyncTimelineData: Async<TimelineData> = Uninitialized
|
val asyncTimelineData: Async<TimelineData> = Uninitialized,
|
||||||
|
val sendMode: SendMode = SendMode.REGULAR,
|
||||||
|
val selectedEvent: TimelineEvent? = null
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -21,8 +21,14 @@ import com.airbnb.mvrx.ViewModelContext
|
|||||||
import im.vector.matrix.android.api.session.Session
|
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.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
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 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 org.koin.android.ext.android.get
|
||||||
|
import ru.noties.markwon.Markwon
|
||||||
|
import ru.noties.markwon.html.HtmlPlugin
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -31,7 +37,7 @@ import java.util.*
|
|||||||
data class MessageActionState(
|
data class MessageActionState(
|
||||||
val userId: String,
|
val userId: String,
|
||||||
val senderName: String,
|
val senderName: String,
|
||||||
val messageBody: String,
|
val messageBody: CharSequence,
|
||||||
val ts: String?,
|
val ts: String?,
|
||||||
val senderAvatarPath: String? = null)
|
val senderAvatarPath: String? = null)
|
||||||
: MvRxState
|
: MvRxState
|
||||||
@ -54,10 +60,19 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode
|
|||||||
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
|
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||||
?: event.root.content.toModel()
|
?: event.root.content.toModel()
|
||||||
val originTs = event.root.originServerTs
|
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(
|
MessageActionState(
|
||||||
event.root.sender ?: "",
|
event.root.sender ?: "",
|
||||||
parcel.informationData.memberName.toString(),
|
parcel.informationData.memberName.toString(),
|
||||||
messageContent?.body ?: "",
|
body,
|
||||||
dateFormat.format(Date(originTs ?: 0)),
|
dateFormat.format(Date(originTs ?: 0)),
|
||||||
currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl)
|
currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl)
|
||||||
)
|
)
|
||||||
|
@ -50,16 +50,17 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
|
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
val messageContent: MessageContent = event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||||
|
?: event.root.content.toModel() ?: return null
|
||||||
val type = messageContent.type
|
val type = messageContent.type
|
||||||
|
|
||||||
if (event.sendState == SendState.UNSENT) {
|
if (event.sendState == SendState.UNSENT) {
|
||||||
//Resend and Delete
|
//Resend and Delete
|
||||||
return MessageMenuState(
|
return MessageMenuState(
|
||||||
listOf(
|
listOf(
|
||||||
SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_corner_down_right, event.root.eventId),
|
SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId),
|
||||||
//TODO delete icon
|
//TODO delete icon
|
||||||
SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_material_delete, event.root.eventId)
|
SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -67,14 +68,29 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
|
|
||||||
//TODO determine if can copy, forward, reply, quote, report?
|
//TODO determine if can copy, forward, reply, quote, report?
|
||||||
val actions = ArrayList<SimpleAction>().apply {
|
val actions = ArrayList<SimpleAction>().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)) {
|
if (canCopy(type)) {
|
||||||
//TODO copy images? html? see ClipBoard
|
//TODO copy images? html? see ClipBoard
|
||||||
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent.body))
|
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)) {
|
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)) {
|
if (canQuote(event, messageContent)) {
|
||||||
@ -82,9 +98,6 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, parcel.eventId))
|
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, parcel.eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canReply(event, messageContent)) {
|
|
||||||
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_corner_down_right))
|
|
||||||
}
|
|
||||||
if (canShare(type)) {
|
if (canShare(type)) {
|
||||||
if (messageContent is MessageImageContent) {
|
if (messageContent is MessageImageContent) {
|
||||||
this.add(
|
this.add(
|
||||||
@ -96,8 +109,6 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
//TODO
|
//TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO is uploading
|
|
||||||
//TODO is downloading
|
|
||||||
|
|
||||||
if (event.sendState == SendState.SENT) {
|
if (event.sendState == SendState.SENT) {
|
||||||
|
|
||||||
@ -159,6 +170,17 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
return event.root.sender == myUserId
|
return event.root.sender == myUserId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
|
||||||
|
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||||
|
if (event.root.type != EventType.MESSAGE) return false
|
||||||
|
//TODO if user is admin or moderator
|
||||||
|
val messageContent = event.root.content.toModel<MessageContent>()
|
||||||
|
return event.root.sender == myUserId && (
|
||||||
|
messageContent?.type == MessageType.MSGTYPE_TEXT
|
||||||
|
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun canCopy(type: String): Boolean {
|
private fun canCopy(type: String): Boolean {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
@ -187,6 +209,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
|
|
||||||
const val ACTION_ADD_REACTION = "add_reaction"
|
const val ACTION_ADD_REACTION = "add_reaction"
|
||||||
const val ACTION_COPY = "copy"
|
const val ACTION_COPY = "copy"
|
||||||
|
const val ACTION_EDIT = "edit"
|
||||||
const val ACTION_QUOTE = "quote"
|
const val ACTION_QUOTE = "quote"
|
||||||
const val ACTION_REPLY = "reply"
|
const val ACTION_REPLY = "reply"
|
||||||
const val ACTION_SHARE = "share"
|
const val ACTION_SHARE = "share"
|
||||||
|
@ -38,6 +38,7 @@ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
|||||||
import im.vector.riotredesign.core.extensions.localDateTime
|
import im.vector.riotredesign.core.extensions.localDateTime
|
||||||
import im.vector.riotredesign.core.linkify.VectorLinkify
|
import im.vector.riotredesign.core.linkify.VectorLinkify
|
||||||
import im.vector.riotredesign.core.resources.ColorProvider
|
import im.vector.riotredesign.core.resources.ColorProvider
|
||||||
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
import im.vector.riotredesign.core.utils.DebouncedClickListener
|
import im.vector.riotredesign.core.utils.DebouncedClickListener
|
||||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||||
@ -52,7 +53,8 @@ import me.gujun.android.span.span
|
|||||||
class MessageItemFactory(private val colorProvider: ColorProvider,
|
class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||||
private val timelineDateFormatter: TimelineDateFormatter,
|
private val timelineDateFormatter: TimelineDateFormatter,
|
||||||
private val htmlRenderer: EventHtmlRenderer) {
|
private val htmlRenderer: EventHtmlRenderer,
|
||||||
|
private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
@ -88,7 +90,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
memberName = formattedMemberName,
|
memberName = formattedMemberName,
|
||||||
showInformation = showInformation,
|
showInformation = showInformation,
|
||||||
orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) },
|
orderedReactionList = event.annotations?.reactionsSummary?.map {
|
||||||
|
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
|
||||||
|
},
|
||||||
hasBeenEdited = hasBeenEdited
|
hasBeenEdited = hasBeenEdited
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -100,11 +104,11 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
val messageContent: MessageContent =
|
val messageContent: MessageContent =
|
||||||
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||||
?: event.root.content.toModel()
|
?: event.root.content.toModel()
|
||||||
?: return null
|
?: //Malformed content, we should echo something on screen
|
||||||
|
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
||||||
|
|
||||||
if (messageContent.relatesTo?.type == RelationType.REPLACE) {
|
if (messageContent.relatesTo?.type == RelationType.REPLACE) {
|
||||||
//TODO blank item or ignore??
|
// ignore replace event, the targeted id is already edited
|
||||||
// ignore this event
|
|
||||||
return BlankItem_()
|
return BlankItem_()
|
||||||
}
|
}
|
||||||
// val all = event.root.toContent()
|
// val all = event.root.toContent()
|
||||||
|
@ -17,8 +17,10 @@
|
|||||||
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
package im.vector.riotredesign.features.home.room.detail.timeline.helper
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.core.extensions.localDateTime
|
import im.vector.riotredesign.core.extensions.localDateTime
|
||||||
|
|
||||||
@ -42,14 +44,25 @@ object TimelineDisplayableEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineEvent.isDisplayable(): Boolean {
|
fun TimelineEvent.isDisplayable(): Boolean {
|
||||||
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type) && !root.content.isNullOrEmpty()
|
if (!TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type)) {
|
||||||
}
|
return false
|
||||||
|
|
||||||
fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
|
||||||
return this.filter {
|
|
||||||
it.isDisplayable()
|
|
||||||
}
|
}
|
||||||
|
if (root.content.isNullOrEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
//Edits should be filtered out!
|
||||||
|
if (EventType.MESSAGE == root.type
|
||||||
|
&& root.content.toModel<MessageContent>()?.relatesTo?.type == RelationType.REPLACE) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
//fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
||||||
|
// return this.filter {
|
||||||
|
// it.isDisplayable()
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
fun TimelineEvent.senderAvatar(): String? {
|
fun TimelineEvent.senderAvatar(): String? {
|
||||||
// We might have no avatar when user leave, so we try to get it from prevContent
|
// We might have no avatar when user leave, so we try to get it from prevContent
|
||||||
|
@ -112,11 +112,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
|
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
|
||||||
reactionButton.isVisible = true
|
reactionButton.isVisible = true
|
||||||
reactionButton.reactedListener = reactionClickListener
|
reactionButton.reactedListener = reactionClickListener
|
||||||
reactionButton.setTag(R.id.messageBottomInfo, reaction.first)
|
reactionButton.setTag(R.id.messageBottomInfo, reaction.key)
|
||||||
idToRefInFlow.add(reactionButton.id)
|
idToRefInFlow.add(reactionButton.id)
|
||||||
reactionButton.reactionString = reaction.first
|
reactionButton.reactionString = reaction.key
|
||||||
reactionButton.reactionCount = reaction.second
|
reactionButton.reactionCount = reaction.count
|
||||||
reactionButton.setChecked(reaction.third)
|
reactionButton.setChecked(reaction.addedByMe)
|
||||||
|
reactionButton.isEnabled = reaction.synced
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
|
// Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
|
||||||
|
@ -16,9 +16,8 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@ -31,6 +30,15 @@ data class MessageInformationData(
|
|||||||
val memberName: CharSequence? = null,
|
val memberName: CharSequence? = null,
|
||||||
val showInformation: Boolean = true,
|
val showInformation: Boolean = true,
|
||||||
/*List of reactions (emoji,count,isSelected)*/
|
/*List of reactions (emoji,count,isSelected)*/
|
||||||
var orderedReactionList: List<Triple<String,Int,Boolean>>? = null,
|
var orderedReactionList: List<ReactionInfoData>? = null,
|
||||||
var hasBeenEdited: Boolean = false
|
var hasBeenEdited: Boolean = false
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ReactionInfoData(
|
||||||
|
val key: String,
|
||||||
|
val count: Int,
|
||||||
|
val addedByMe: Boolean,
|
||||||
|
val synced: Boolean
|
||||||
|
) : Parcelable
|
||||||
|
Before Width: | Height: | Size: 394 B |
Before Width: | Height: | Size: 335 B |
Before Width: | Height: | Size: 285 B |
Before Width: | Height: | Size: 257 B |
Before Width: | Height: | Size: 507 B |
Before Width: | Height: | Size: 423 B |
Before Width: | Height: | Size: 809 B |
Before Width: | Height: | Size: 550 B |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 728 B |
54
vector/src/main/res/drawable/ic_add_reaction.xml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="22dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="22">
|
||||||
|
<path
|
||||||
|
android:pathData="m13.5909,20.8117c-0.8681,0.2472 -1.7837,0.3795 -2.7298,0.3795 -5.5574,0 -10.0625,-4.5627 -10.0625,-10.1912 0,-5.6284 4.5051,-10.1912 10.0625,-10.1912 5.4556,0 9.8971,4.3972 10.058,9.8831h-0.9588c-0.1605,-4.9498 -4.1729,-8.9125 -9.0992,-8.9125 -5.0281,0 -9.1042,4.1282 -9.1042,9.2206 0,5.0924 4.0761,9.2206 9.1042,9.2206 0.951,0 1.868,-0.1477 2.7298,-0.4217z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="1.047619"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9e9e9e"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m14.6944,16.8235h7.6667"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2.095238"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9e9e9e"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m18.5278,12.9412l-0,7.7647"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2.095238"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9e9e9e"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m7.0278,12.9412s1.4375,1.9412 3.8333,1.9412c2.3958,0 3.8333,-1.9412 3.8333,-1.9412"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2.095238"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9e9e9e"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m7.9861,8.0882h0.0096"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2.095238"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9e9e9e"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m13.7361,8.0882h0.0096"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2.095238"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9e9e9e"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
14
vector/src/main/res/drawable/ic_attachment.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="21dp"
|
||||||
|
android:height="22dp"
|
||||||
|
android:viewportWidth="21"
|
||||||
|
android:viewportHeight="22">
|
||||||
|
<path
|
||||||
|
android:pathData="M19.468,10.571l-8.73,8.753a5.693,5.693 0,0 1,-8.066 0,5.728 5.728,0 0,1 0,-8.086l8.73,-8.752a3.795,3.795 0,0 1,5.378 0,3.818 3.818,0 0,1 0,5.39L8.04,16.63a1.898,1.898 0,0 1,-2.689 0,1.91 1.91,0 0,1 0,-2.696l8.065,-8.076"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9E9E9E"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
20
vector/src/main/res/drawable/ic_close_round.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="22dp"
|
||||||
|
android:height="22dp"
|
||||||
|
android:viewportWidth="22"
|
||||||
|
android:viewportHeight="22">
|
||||||
|
<path
|
||||||
|
android:pathData="M11,11m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#979797"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M7.667,7.667L14.333,14.333M14.333,7.667L7.667,14.333"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#9E9E9E"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
@ -4,31 +4,19 @@
|
|||||||
android:viewportWidth="22"
|
android:viewportWidth="22"
|
||||||
android:viewportHeight="22">
|
android:viewportHeight="22">
|
||||||
<path
|
<path
|
||||||
android:pathData="M11,11m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
|
android:pathData="M3.222,1L18.778,1A2.222,2.222 0,0 1,21 3.222L21,18.778A2.222,2.222 0,0 1,18.778 21L3.222,21A2.222,2.222 0,0 1,1 18.778L1,3.222A2.222,2.222 0,0 1,3.222 1z"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:strokeWidth="2"
|
android:strokeWidth="2"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:strokeColor="#9E9E9E"
|
|
||||||
android:fillType="evenOdd"
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#9E9E9E"
|
||||||
android:strokeLineCap="round"/>
|
android:strokeLineCap="round"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M7,13C7,13 8.5,15 11,15C13.5,15 15,13 15,13"
|
android:pathData="M7.667,7.667l6.666,6.666M14.333,7.667l-6.666,6.666"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:strokeWidth="2"
|
android:strokeWidth="2"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
android:strokeColor="#9E9E9E"
|
android:strokeColor="#9E9E9E"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:strokeLineCap="round"/>
|
android:strokeLineCap="round"/>
|
||||||
<path
|
|
||||||
android:pathData="M7.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:fillColor="#9E9E9E"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:strokeColor="#00000000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M14.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:fillColor="#9E9E9E"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:strokeColor="#00000000"/>
|
|
||||||
</vector>
|
</vector>
|
@ -4,19 +4,19 @@
|
|||||||
android:viewportWidth="21"
|
android:viewportWidth="21"
|
||||||
android:viewportHeight="22">
|
android:viewportHeight="22">
|
||||||
<path
|
<path
|
||||||
android:pathData="M9.497,3.06H2.888C1.845,3.06 1,3.93 1,5v13.576c0,1.07 0.845,1.94 1.888,1.94h13.218c1.042,0 1.888,-0.87 1.888,-1.94v-6.788"
|
android:pathData="M9.4969,3.0606L2.8882,3.0606C1.8454,3.0606 1,3.9289 1,5L1,18.5758C1,19.6469 1.8454,20.5152 2.8882,20.5152L16.1056,20.5152C17.1484,20.5152 17.9938,19.6469 17.9938,18.5758L17.9938,11.7879"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:strokeWidth="2"
|
android:strokeWidth="2"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:strokeColor="#9E9E9E"
|
android:strokeColor="#9E9E9E"
|
||||||
|
android:fillType="evenOdd"
|
||||||
android:strokeLineCap="round"/>
|
android:strokeLineCap="round"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M16.578,1.606a1.966,1.966 0,0 1,2.832 0,2.097 2.097,0 0,1 0,2.91l-8.969,9.211 -3.776,0.97 0.944,-3.879 8.969,-9.212z"
|
android:pathData="M16.5776,1.6061C17.3598,0.8027 18.6278,0.8027 19.4099,1.6061C20.1921,2.4094 20.1921,3.7118 19.4099,4.5152L10.441,13.7273L6.6646,14.697L7.6087,10.8182L16.5776,1.6061Z"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:strokeWidth="2"
|
android:strokeWidth="2"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:strokeColor="#9E9E9E"
|
android:strokeColor="#9E9E9E"
|
||||||
|
android:fillType="evenOdd"
|
||||||
android:strokeLineCap="round"/>
|
android:strokeLineCap="round"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="22dp"
|
android:width="22dp"
|
||||||
android:height="22dp"
|
android:height="13dp"
|
||||||
android:viewportWidth="22"
|
android:viewportWidth="22"
|
||||||
android:viewportHeight="22">
|
android:viewportHeight="13">
|
||||||
<path
|
<path
|
||||||
android:pathData="M14.75,8.5L21,14.75 14.75,21"
|
android:pathData="M5.4444,1l-4.4444,4.3636l4.4444,4.3636"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:strokeWidth="2"
|
android:strokeWidth="2"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:strokeColor="#9E9E9E"
|
android:strokeColor="#9E9E9E"
|
||||||
|
android:fillType="evenOdd"
|
||||||
android:strokeLineCap="round"/>
|
android:strokeLineCap="round"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M1,1v8.75a5,5 0,0 0,5 5h15"
|
android:pathData="M21,11.9091L21,9.7273C21,7.3173 19.0102,5.3636 16.5556,5.3636L1,5.3636"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:strokeWidth="2"
|
android:strokeWidth="2"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:strokeColor="#9E9E9E"
|
android:strokeColor="#9E9E9E"
|
||||||
|
android:fillType="evenOdd"
|
||||||
android:strokeLineCap="round"/>
|
android:strokeLineCap="round"/>
|
||||||
</vector>
|
</vector>
|
14
vector/src/main/res/drawable/ic_send.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="22dp"
|
||||||
|
android:height="22dp"
|
||||||
|
android:viewportWidth="22"
|
||||||
|
android:viewportHeight="22">
|
||||||
|
<path
|
||||||
|
android:pathData="M20.142,11H4.586M20.142,11L1.05,20.192 4.586,11 1.05,1.808z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#03B381"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
@ -3,7 +3,6 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:layout_height="50dp"
|
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:foreground="?attr/selectableItemBackground"
|
android:foreground="?attr/selectableItemBackground"
|
||||||
@ -12,7 +11,8 @@
|
|||||||
android:paddingLeft="@dimen/layout_horizontal_margin"
|
android:paddingLeft="@dimen/layout_horizontal_margin"
|
||||||
android:paddingTop="8dp"
|
android:paddingTop="8dp"
|
||||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||||
android:paddingBottom="8dp">
|
android:paddingBottom="8dp"
|
||||||
|
tools:layout_height="50dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/action_icon"
|
android:id="@+id/action_icon"
|
||||||
@ -21,8 +21,8 @@
|
|||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginRight="16dp"
|
android:layout_marginRight="16dp"
|
||||||
android:src="@drawable/ic_material_delete"
|
tools:src="@drawable/ic_delete"
|
||||||
android:tint="?android:attr/textColorSecondary" />
|
android:tint="?android:attr/textColorTertiary" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/action_title"
|
android:id="@+id/action_title"
|
||||||
|
@ -0,0 +1,164 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/composerLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_backround"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?vctr_bottom_nav_background_color"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:layout_height="40dp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background_top_separator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?vctr_bottom_nav_background_border_color"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background_bottom_separator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?vctr_bottom_nav_background_border_color"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composer_related_message_avatar_view"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="parent"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composer_related_message_sender"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/composer_related_message_preview"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="@tools:sample/first_names" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composer_related_message_preview"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composer_related_message_action_image"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="38dp"
|
||||||
|
android:alpha="0"
|
||||||
|
android:tint="?android:attr/textColorTertiary"
|
||||||
|
app:layout_constraintEnd_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="parent"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:src="@drawable/ic_edit" />
|
||||||
|
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composer_related_message_close"
|
||||||
|
android:layout_width="22dp"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_close_round"
|
||||||
|
android:tint="@color/rosy_pink"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composer_avatar_view"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/composerEditText"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_attachment"
|
||||||
|
android:tint="?attr/colorAccent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/composerEditText" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/composer_preview_barrier"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:barrierMargin="8dp"
|
||||||
|
app:constraint_referenced_ids="composer_related_message_preview,composer_related_message_action_image"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/sendButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
android:tint="?attr/colorAccent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/attachmentButton" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/composerEditText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:hint="@string/room_message_placeholder_not_encrypted"
|
||||||
|
android:maxHeight="200dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:nextFocusLeft="@id/composerEditText"
|
||||||
|
android:nextFocusUp="@id/composerEditText"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/attachmentButton"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/composer_avatar_view"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
<!--tools:text="@tools:sample/lorem/random"-->
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,173 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/composerLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_backround"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?vctr_bottom_nav_background_color"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composer_preview_barrier"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background_top_separator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?vctr_bottom_nav_background_border_color"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/related_message_backround"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/related_message_backround"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/related_message_backround" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background_bottom_separator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?vctr_bottom_nav_background_border_color"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/related_message_backround"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/related_message_backround"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/related_message_backround" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composer_related_message_avatar_view"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/composer_related_message_action_image"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/composer_related_message_sender"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composer_related_message_sender"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composer_related_message_sender"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/composer_related_message_close"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/composer_related_message_avatar_view"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@tools:sample/first_names" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composer_related_message_preview"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="?vctr_message_text_color"
|
||||||
|
app:layout_constrainedHeight="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/composer_related_message_sender"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composer_related_message_sender"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composer_related_message_sender"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composer_related_message_action_image"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginBottom="38dp"
|
||||||
|
android:alpha="1"
|
||||||
|
android:tint="?android:attr/textColorTertiary"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/composer_related_message_avatar_view"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composer_related_message_avatar_view"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composer_related_message_avatar_view"
|
||||||
|
tools:src="@drawable/ic_edit" />
|
||||||
|
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composer_related_message_close"
|
||||||
|
android:layout_width="22dp"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_close_round"
|
||||||
|
android:tint="@color/rosy_pink"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composer_related_message_preview"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composer_related_message_preview" />
|
||||||
|
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composer_avatar_view"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/composerEditText"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_attachment"
|
||||||
|
android:tint="?attr/colorAccent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
||||||
|
app:layout_constraintTop_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/composer_preview_barrier"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:barrierMargin="8dp"
|
||||||
|
app:constraint_referenced_ids="composer_related_message_preview,composer_related_message_action_image"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/sendButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
android:tint="?attr/colorAccent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/composerEditText"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
||||||
|
app:layout_constraintVertical_bias="1" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/composerEditText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:hint="@string/room_message_placeholder_not_encrypted"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:nextFocusLeft="@id/composerEditText"
|
||||||
|
android:nextFocusUp="@id/composerEditText"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/composer_avatar_view"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
||||||
|
tools:text="@tools:sample/lorem" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -2,6 +2,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/rootConstraintLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
@ -76,69 +77,20 @@
|
|||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/composerDivider"
|
app:layout_constraintBottom_toTopOf="@+id/composerLayout"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
|
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
|
||||||
tools:listitem="@layout/item_timeline_event_base" />
|
tools:listitem="@layout/item_timeline_event_base" />
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/composerDivider"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="?vctr_list_divider_color"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/composerLayout" />
|
|
||||||
|
|
||||||
<RelativeLayout
|
<im.vector.riotredesign.features.home.room.detail.composer.TextComposerView
|
||||||
android:id="@+id/composerLayout"
|
android:id="@+id/composerLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent">
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/attachmentButton"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_toStartOf="@id/sendButton"
|
|
||||||
android:layout_toLeftOf="@id/sendButton"
|
|
||||||
android:background="?android:attr/selectableItemBackground"
|
|
||||||
android:src="@drawable/ic_attach_file_white"
|
|
||||||
android:tint="?attr/colorAccent" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/sendButton"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:background="?android:attr/selectableItemBackground"
|
|
||||||
android:src="@drawable/ic_send_white"
|
|
||||||
android:tint="?attr/colorAccent" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/composerEditText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_alignParentLeft="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_toStartOf="@id/attachmentButton"
|
|
||||||
android:layout_toLeftOf="@id/attachmentButton"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:hint="@string/room_message_placeholder_not_encrypted"
|
|
||||||
android:inputType="textCapSentences|textMultiLine"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
android:nextFocusLeft="@id/composerEditText"
|
|
||||||
android:nextFocusUp="@id/composerEditText"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:textSize="14sp" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<im.vector.riotredesign.features.invite.VectorInviteView
|
<im.vector.riotredesign.features.invite.VectorInviteView
|
||||||
android:id="@+id/inviteView"
|
android:id="@+id/inviteView"
|
||||||
|
127
vector/src/main/res/layout/merge_composer_layout.xml
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:constraintSet="@layout/constraint_set_composer_layout_compact"
|
||||||
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||||
|
|
||||||
|
<!-- ========================
|
||||||
|
/!\ Constraints for this layout are defined in external layout files that are used as constraint set for animation.
|
||||||
|
/!\ These 3 files must be modified to stay coherent!
|
||||||
|
======================== -->
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_backround"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?vctr_bottom_nav_background_color"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background_top_separator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?vctr_bottom_nav_background_border_color"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background_bottom_separator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?vctr_bottom_nav_background_border_color"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composer_related_message_avatar_view"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composer_related_message_sender"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:text="@tools:sample/first_names"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composer_related_message_preview"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="3"
|
||||||
|
android:textColor="?vctr_message_text_color"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:text="@tools:sample/lorem"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composer_related_message_action_image"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:tint="?android:attr/textColorTertiary"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composer_related_message_close"
|
||||||
|
android:layout_width="22dp"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_close_round"
|
||||||
|
android:tint="@color/rosy_pink"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composer_avatar_view"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_attachment"
|
||||||
|
android:tint="?attr/colorAccent"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/composer_preview_barrier"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:barrierMargin="8dp"
|
||||||
|
app:constraint_referenced_ids="composer_related_message_preview,composer_related_message_action_image"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/sendButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
android:tint="?attr/colorAccent"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/composerEditText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:hint="@string/room_message_placeholder_not_encrypted"
|
||||||
|
android:nextFocusLeft="@id/composerEditText"
|
||||||
|
android:nextFocusUp="@id/composerEditText"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:textColor="?vctr_message_text_color"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
</merge>
|
@ -4,6 +4,7 @@
|
|||||||
<declare-styleable name="VectorStyles">
|
<declare-styleable name="VectorStyles">
|
||||||
|
|
||||||
<attr name="vctr_bottom_nav_background_color" format="color" />
|
<attr name="vctr_bottom_nav_background_color" format="color" />
|
||||||
|
<attr name="vctr_bottom_nav_background_border_color" format="color" />
|
||||||
|
|
||||||
<!-- waiting view background -->
|
<!-- waiting view background -->
|
||||||
<attr name="vctr_waiting_background_color" format="color" />
|
<attr name="vctr_waiting_background_color" format="color" />
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
<color name="tab_rooms">@color/accent_color_light</color>
|
<color name="tab_rooms">@color/accent_color_light</color>
|
||||||
<color name="tab_rooms_secondary">#5EA584</color>
|
<color name="tab_rooms_secondary">#5EA584</color>
|
||||||
<color name="tab_groups">#a6d0e5</color>
|
<color name="tab_groups">#a6d0e5</color>
|
||||||
|
|
||||||
<color name="tab_groups_secondary">#81bddb</color>
|
<color name="tab_groups_secondary">#81bddb</color>
|
||||||
|
|
||||||
<!-- color of the direct chat avatar ring (it's 50% of color accent) -->
|
<!-- color of the direct chat avatar ring (it's 50% of color accent) -->
|
||||||
|
@ -26,10 +26,9 @@
|
|||||||
<string name="last_edited_info_message">Last edited by %s on %s</string>
|
<string name="last_edited_info_message">Last edited by %s on %s</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="malformed_message">Malformed event, cannot display</string>
|
||||||
<string name="create_new_room">Create New Room</string>
|
<string name="create_new_room">Create New Room</string>
|
||||||
|
|
||||||
<string name="error_no_network">No network. Please check your Internet connection.</string>
|
<string name="error_no_network">No network. Please check your Internet connection.</string>
|
||||||
|
|
||||||
<string name="action_change">"Change"</string>
|
<string name="action_change">"Change"</string>
|
||||||
<string name="change_room_directory_network">"Change network"</string>
|
<string name="change_room_directory_network">"Change network"</string>
|
||||||
<string name="please_wait">"Please wait…"</string>
|
<string name="please_wait">"Please wait…"</string>
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
<!-- activities background -->
|
<!-- activities background -->
|
||||||
<item name="android:windowBackground">@color/riot_primary_background_color_black</item>
|
<item name="android:windowBackground">@color/riot_primary_background_color_black</item>
|
||||||
<item name="vctr_bottom_nav_background_color">@color/primary_color_black</item>
|
<item name="vctr_bottom_nav_background_color">@color/primary_color_black</item>
|
||||||
|
<item name="vctr_bottom_nav_background_border_color">#FFE9EDF1</item>
|
||||||
|
|
||||||
<item name="vctr_direct_chat_circle">@drawable/direct_chat_circle_black</item>
|
<item name="vctr_direct_chat_circle">@drawable/direct_chat_circle_black</item>
|
||||||
</style>
|
</style>
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
<!-- default background color -->
|
<!-- default background color -->
|
||||||
<item name="android:colorBackground">@color/riot_primary_background_color_dark</item>
|
<item name="android:colorBackground">@color/riot_primary_background_color_dark</item>
|
||||||
<item name="vctr_bottom_nav_background_color">@color/primary_color_dark</item>
|
<item name="vctr_bottom_nav_background_color">@color/primary_color_dark</item>
|
||||||
|
<item name="vctr_bottom_nav_background_border_color">#FFE9EDF1</item>
|
||||||
|
|
||||||
<!-- waiting view background -->
|
<!-- waiting view background -->
|
||||||
<item name="vctr_waiting_background_color">#55555555</item>
|
<item name="vctr_waiting_background_color">#55555555</item>
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
<!-- default background color -->
|
<!-- default background color -->
|
||||||
<item name="android:colorBackground">@color/riot_primary_background_color_light</item>
|
<item name="android:colorBackground">@color/riot_primary_background_color_light</item>
|
||||||
<item name="vctr_bottom_nav_background_color">#FFF3F8FD</item>
|
<item name="vctr_bottom_nav_background_color">#FFF3F8FD</item>
|
||||||
|
<item name="vctr_bottom_nav_background_border_color">#FFE9EDF1</item>
|
||||||
|
|
||||||
<!-- default button -->
|
<!-- default button -->
|
||||||
<item name="android:buttonStyle">@style/Widget.Vector.Button</item>
|
<item name="android:buttonStyle">@style/Widget.Vector.Button</item>
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
<item name="android:colorBackground">@color/riot_primary_background_color_status</item>
|
<item name="android:colorBackground">@color/riot_primary_background_color_status</item>
|
||||||
<item name="vctr_bottom_nav_background_color">@color/riot_primary_background_color_status
|
<item name="vctr_bottom_nav_background_color">@color/riot_primary_background_color_status
|
||||||
</item>
|
</item>
|
||||||
|
<item name="vctr_bottom_nav_background_border_color">#FFE9EDF1</item>
|
||||||
|
|
||||||
<item name="buttonStyle">@style/Widget.Vector.Button</item>
|
<item name="buttonStyle">@style/Widget.Vector.Button</item>
|
||||||
|
|
||||||
|