forked from GitHub-Mirror/riotX-android
Merge pull request #139 from vector-im/feature/undo_reaction
Undo Reaction
This commit is contained in:
commit
2581bf69e0
@ -18,10 +18,12 @@ 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.session.room.EventRelationExtractor
|
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
|
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||||
|
import im.vector.matrix.android.internal.session.room.members.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
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||||
@ -55,7 +57,8 @@ 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 tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
|
val erau = EventRelationsAggregationUpdater(Credentials("", "", "", null, null))
|
||||||
|
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)
|
||||||
|
@ -25,5 +25,5 @@ data class UnsignedData(
|
|||||||
@Json(name = "redacted_because") val redactedEvent: Event? = null,
|
@Json(name = "redacted_because") val redactedEvent: Event? = null,
|
||||||
@Json(name = "transaction_id") val transactionId: String? = null,
|
@Json(name = "transaction_id") val transactionId: String? = null,
|
||||||
@Json(name = "prev_content") val prevContent: Map<String, Any>? = null,
|
@Json(name = "prev_content") val prevContent: Map<String, Any>? = null,
|
||||||
@Json(name = "m.relations") val relations: AggregatedRelations?
|
@Json(name = "m.relations") val relations: AggregatedRelations? = null
|
||||||
)
|
)
|
@ -19,6 +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.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
|
||||||
@ -27,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 {
|
interface Room : TimelineService, SendService, ReadService, MembershipService, StateService , ReactionService{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The roomId of this room
|
* The roomId of this room
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
|
||||||
|
}
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.matrix.android.api.session.room.send
|
package im.vector.matrix.android.api.session.room.send
|
||||||
|
|
||||||
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.room.model.message.MessageType
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
|
||||||
@ -48,7 +49,6 @@ interface SendService {
|
|||||||
*/
|
*/
|
||||||
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
|
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
|
||||||
|
|
||||||
|
fun redactEvent(event: Event, reason: String?): Cancelable
|
||||||
fun sendReaction(reaction: String, targetEventId: String) : Cancelable
|
|
||||||
|
|
||||||
}
|
}
|
@ -154,7 +154,7 @@ 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())
|
val eventsPruner = EventsPruner(get(), get(), get(), get())
|
||||||
val userEntityUpdater = UserEntityUpdater(get(), get(), get())
|
val userEntityUpdater = UserEntityUpdater(get(), get(), get())
|
||||||
listOf<LiveEntityObserver>(groupSummaryUpdater, eventsPruner, userEntityUpdater)
|
listOf<LiveEntityObserver>(groupSummaryUpdater, eventsPruner, userEntityUpdater)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +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.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
|
||||||
@ -39,12 +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 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,
|
||||||
MembershipService by roomMembersService {
|
MembershipService by roomMembersService {
|
||||||
|
|
||||||
override val roomSummary: LiveData<RoomSummary> by lazy {
|
override val roomSummary: LiveData<RoomSummary> by lazy {
|
||||||
|
@ -67,13 +67,14 @@ internal class EventRelationsAggregationUpdater(private val credentials: Credent
|
|||||||
sum.key = reaction
|
sum.key = reaction
|
||||||
sum.firstTimestamp = event.originServerTs ?: 0
|
sum.firstTimestamp = event.originServerTs ?: 0
|
||||||
sum.count = 1
|
sum.count = 1
|
||||||
|
sum.sourceEvents.add(event.eventId)
|
||||||
sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender)
|
sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender)
|
||||||
eventSummary.reactionsSummary.add(sum)
|
eventSummary.reactionsSummary.add(sum)
|
||||||
} else {
|
} else {
|
||||||
//is this a known event (is possible? pagination?)
|
//is this a known event (is possible? pagination?)
|
||||||
if (!sum.sourceEvents.contains(eventId)) {
|
if (!sum.sourceEvents.contains(eventId)) {
|
||||||
sum.count += 1
|
sum.count += 1
|
||||||
sum.sourceEvents.add(eventId)
|
sum.sourceEvents.add(event.eventId)
|
||||||
sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender)
|
sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,4 +197,22 @@ internal interface RoomAPI {
|
|||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave")
|
||||||
fun leave(@Path("roomId") roomId: String,
|
fun leave(@Path("roomId") roomId: String,
|
||||||
@Body params: Map<String, String>): Call<Unit>
|
@Body params: Map<String, String>): Call<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room.
|
||||||
|
* This cannot be undone.
|
||||||
|
* Users may redact their own events, and any user with a power level greater than or equal to the redact power level of the room may redact events there.
|
||||||
|
*
|
||||||
|
* @param txId the transaction Id
|
||||||
|
* @param roomId the room id
|
||||||
|
* @param eventId the event to delete
|
||||||
|
* @param reason json containing reason key {"reason": "Indecent material"}
|
||||||
|
*/
|
||||||
|
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}")
|
||||||
|
fun redactEvent(
|
||||||
|
@Path("txnId") txId: String,
|
||||||
|
@Path("roomId") roomId: String,
|
||||||
|
@Path("eventId") parent_id: String,
|
||||||
|
@Body reason: Map<String, String>
|
||||||
|
): Call<SendResponse>
|
||||||
}
|
}
|
@ -19,6 +19,9 @@ 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.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
|
||||||
@ -45,6 +48,8 @@ internal class RoomFactory(private val monarchy: Monarchy,
|
|||||||
private val paginationTask: PaginationTask,
|
private val paginationTask: PaginationTask,
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val setReadMarkersTask: SetReadMarkersTask,
|
private val setReadMarkersTask: SetReadMarkersTask,
|
||||||
|
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
||||||
|
private val updateQuickReactionTask: UpdateQuickReactionTask,
|
||||||
private val joinRoomTask: JoinRoomTask,
|
private val joinRoomTask: JoinRoomTask,
|
||||||
private val leaveRoomTask: LeaveRoomTask) {
|
private val leaveRoomTask: LeaveRoomTask) {
|
||||||
|
|
||||||
@ -53,6 +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 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)
|
||||||
@ -64,6 +70,7 @@ internal class RoomFactory(private val monarchy: Monarchy,
|
|||||||
sendService,
|
sendService,
|
||||||
stateService,
|
stateService,
|
||||||
readService,
|
readService,
|
||||||
|
reactionService,
|
||||||
roomMembersService
|
roomMembersService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,10 @@
|
|||||||
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
|
||||||
@ -27,16 +31,14 @@ import im.vector.matrix.android.internal.session.room.membership.joining.InviteT
|
|||||||
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask
|
import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
|
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask
|
||||||
|
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.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
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
|
import im.vector.matrix.android.internal.session.room.timeline.*
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
|
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
|
||||||
import org.koin.dsl.module.module
|
import org.koin.dsl.module.module
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
@ -75,7 +77,7 @@ class RoomModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
|
RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
@ -98,5 +100,17 @@ class RoomModule {
|
|||||||
DefaultSendStateTask(get()) as SendStateTask
|
DefaultSendStateTask(get()) as SendStateTask
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope(DefaultSession.SCOPE) {
|
||||||
|
DefaultFindReactionEventForUndoTask(get()) as FindReactionEventForUndoTask
|
||||||
|
}
|
||||||
|
|
||||||
|
scope(DefaultSession.SCOPE) {
|
||||||
|
DefaultUpdateQuickReactionTask(get()) as UpdateQuickReactionTask
|
||||||
|
}
|
||||||
|
|
||||||
|
scope(DefaultSession.SCOPE) {
|
||||||
|
DefaultPruneEventTask(get()) as PruneEventTask
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* 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 arrow.core.Try
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import io.realm.Realm
|
||||||
|
|
||||||
|
|
||||||
|
internal interface FindReactionEventForUndoTask : Task<FindReactionEventForUndoTask.Params, FindReactionEventForUndoTask.Result> {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val roomId: String,
|
||||||
|
val eventId: String,
|
||||||
|
val reaction: String,
|
||||||
|
val myUserId: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Result(
|
||||||
|
val redactEventId: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultFindReactionEventForUndoTask(private val monarchy: Monarchy) : FindReactionEventForUndoTask {
|
||||||
|
|
||||||
|
override fun execute(params: FindReactionEventForUndoTask.Params): Try<FindReactionEventForUndoTask.Result> {
|
||||||
|
return Try {
|
||||||
|
var eventId: String? = null
|
||||||
|
monarchy.doWithRealm { realm ->
|
||||||
|
eventId = getReactionToRedact(realm, params.reaction, params.eventId, params.myUserId)?.eventId
|
||||||
|
}
|
||||||
|
FindReactionEventForUndoTask.Result(eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? {
|
||||||
|
val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
if (summary != null) {
|
||||||
|
summary.reactionsSummary.where()
|
||||||
|
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
|
||||||
|
.findFirst()?.let {
|
||||||
|
//want to find the event orignated by me!
|
||||||
|
it.sourceEvents.forEach {
|
||||||
|
//find source event
|
||||||
|
EventEntity.where(realm, it).findFirst()?.let { eventEntity ->
|
||||||
|
//is it mine?
|
||||||
|
if (eventEntity.sender == userId) {
|
||||||
|
return eventEntity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,25 @@
|
|||||||
package im.vector.matrix.android.internal.session.room.send
|
/*
|
||||||
|
* 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 android.content.Context
|
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.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.annotation.ReactionContent
|
||||||
@ -11,6 +27,7 @@ import im.vector.matrix.android.api.session.room.model.annotation.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
|
||||||
|
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||||
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
|
||||||
|
|
||||||
@ -28,7 +45,7 @@ class SendRelationWorker(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<SendRelationWorker.Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
|
||||||
val localEvent = params.event
|
val localEvent = params.event
|
||||||
@ -50,6 +67,11 @@ class SendRelationWorker(context: Context, params: WorkerParameters)
|
|||||||
content = localEvent.content
|
content = localEvent.content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return result.fold({ Result.retry() }, { Result.success() })
|
return result.fold({
|
||||||
|
when (it) {
|
||||||
|
is Failure.NetworkConnection -> Result.retry()
|
||||||
|
else -> Result.failure()
|
||||||
|
}
|
||||||
|
}, { Result.success() })
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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 arrow.core.Try
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import io.realm.Realm
|
||||||
|
|
||||||
|
|
||||||
|
internal interface UpdateQuickReactionTask : Task<UpdateQuickReactionTask.Params, UpdateQuickReactionTask.Result> {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val roomId: String,
|
||||||
|
val eventId: String,
|
||||||
|
val reaction: String,
|
||||||
|
val oppositeReaction: String,
|
||||||
|
val myUserId: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Result(
|
||||||
|
val reactionToAdd: String?,
|
||||||
|
val reactionToRedact: List<String>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultUpdateQuickReactionTask(private val monarchy: Monarchy) : UpdateQuickReactionTask {
|
||||||
|
override fun execute(params: UpdateQuickReactionTask.Params): Try<UpdateQuickReactionTask.Result> {
|
||||||
|
return Try {
|
||||||
|
var res: Pair<String?, List<String>?>? = null
|
||||||
|
monarchy.doWithRealm { realm ->
|
||||||
|
res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId, params.myUserId)
|
||||||
|
}
|
||||||
|
UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String, myUserId: String): Pair<String?, List<String>?> {
|
||||||
|
//the emoji reaction has been selected, we need to check if we have reacted it or not
|
||||||
|
val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
?: return Pair(reaction, null)
|
||||||
|
|
||||||
|
//Ok there is already reactions on this event, have we reacted to it
|
||||||
|
val aggregationForReaction = existingSummary.reactionsSummary.where()
|
||||||
|
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
|
||||||
|
.findFirst()
|
||||||
|
val aggregationForOppositeReaction = existingSummary.reactionsSummary.where()
|
||||||
|
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, oppositeReaction)
|
||||||
|
.findFirst()
|
||||||
|
|
||||||
|
if (aggregationForReaction == null || !aggregationForReaction.addedByMe) {
|
||||||
|
//i haven't yet reacted to it, so need to add it, but do I need to redact the opposite?
|
||||||
|
val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull {
|
||||||
|
//find source event
|
||||||
|
val entity = EventEntity.where(realm, it).findFirst()
|
||||||
|
if (entity?.sender == myUserId) entity.eventId else null
|
||||||
|
}
|
||||||
|
return Pair(reaction, toRedact)
|
||||||
|
} else {
|
||||||
|
//I already added it, so i need to undo it (like a toggle)
|
||||||
|
// find all m.redaction coming from me to readact them
|
||||||
|
val toRedact = aggregationForReaction.sourceEvents.mapNotNull {
|
||||||
|
//find source event
|
||||||
|
val entity = EventEntity.where(realm, it).findFirst()
|
||||||
|
if (entity?.sender == myUserId) entity.eventId else null
|
||||||
|
}
|
||||||
|
return Pair(null, toRedact)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -16,38 +16,37 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.prune
|
package im.vector.matrix.android.internal.session.room.prune
|
||||||
|
|
||||||
import androidx.work.ExistingWorkPolicy
|
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
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.internal.database.RealmLiveEntityObserver
|
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.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.util.WorkerParamsFactory
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
|
|
||||||
private const val PRUNE_EVENT_WORKER = "PRUNE_EVENT_WORKER"
|
|
||||||
|
|
||||||
internal class EventsPruner(monarchy: Monarchy) :
|
internal class EventsPruner(monarchy: Monarchy,
|
||||||
|
private val credentials: Credentials,
|
||||||
|
private val pruneEventTask: PruneEventTask,
|
||||||
|
private val taskExecutor: TaskExecutor) :
|
||||||
RealmLiveEntityObserver<EventEntity>(monarchy) {
|
RealmLiveEntityObserver<EventEntity>(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>) {
|
||||||
val redactionEvents = inserted
|
val redactionEvents = inserted
|
||||||
.mapNotNull { it.asDomain().redacts }
|
.mapNotNull { it.asDomain() }
|
||||||
|
|
||||||
val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents)
|
val params = PruneEventTask.Params(
|
||||||
val workData = WorkerParamsFactory.toData(pruneEventWorkerParams)
|
redactionEvents,
|
||||||
|
credentials.userId
|
||||||
|
)
|
||||||
|
|
||||||
val sendWork = OneTimeWorkRequestBuilder<PruneEventWorker>()
|
pruneEventTask.configureWith(params)
|
||||||
.setInputData(workData)
|
.executeBy(taskExecutor)
|
||||||
.build()
|
|
||||||
|
|
||||||
WorkManager.getInstance()
|
|
||||||
.beginUniqueWork(PRUNE_EVENT_WORKER, ExistingWorkPolicy.APPEND, sendWork)
|
|
||||||
.enqueue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* 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.prune
|
||||||
|
|
||||||
|
import arrow.core.Try
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent
|
||||||
|
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.EventAnnotationsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
|
import io.realm.Realm
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
internal interface PruneEventTask : Task<PruneEventTask.Params, Unit> {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val redactionEvents: List<Event>,
|
||||||
|
val userId: String
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultPruneEventTask(private val monarchy: Monarchy) : PruneEventTask {
|
||||||
|
|
||||||
|
override fun execute(params: PruneEventTask.Params): Try<Unit> {
|
||||||
|
return monarchy.tryTransactionSync { realm ->
|
||||||
|
params.redactionEvents.forEach { event ->
|
||||||
|
pruneEvent(realm, event, params.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pruneEvent(realm: Realm, redactionEvent: Event, userId: String) {
|
||||||
|
if (redactionEvent.redacts.isNullOrBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
|
||||||
|
?: return
|
||||||
|
|
||||||
|
val allowedKeys = computeAllowedKeys(eventToPrune.type)
|
||||||
|
if (allowedKeys.isNotEmpty()) {
|
||||||
|
val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
|
||||||
|
eventToPrune.content = ContentMapper.map(prunedContent)
|
||||||
|
} else {
|
||||||
|
when (eventToPrune.type) {
|
||||||
|
EventType.MESSAGE -> {
|
||||||
|
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||||
|
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||||
|
?: UnsignedData(null, null)
|
||||||
|
val modified = unsignedData.copy(redactedEvent = redactionEvent)
|
||||||
|
eventToPrune.content = ContentMapper.map(emptyMap())
|
||||||
|
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
|
||||||
|
|
||||||
|
}
|
||||||
|
EventType.REACTION -> {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeAllowedKeys(type: String): List<String> {
|
||||||
|
// Add filtered content, allowed keys in content depends on the event type
|
||||||
|
return when (type) {
|
||||||
|
EventType.STATE_ROOM_MEMBER -> listOf("membership")
|
||||||
|
EventType.STATE_ROOM_CREATE -> listOf("creator")
|
||||||
|
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
|
||||||
|
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
|
||||||
|
"users_default",
|
||||||
|
"events",
|
||||||
|
"events_default",
|
||||||
|
"state_default",
|
||||||
|
"ban",
|
||||||
|
"kick",
|
||||||
|
"redact",
|
||||||
|
"invite")
|
||||||
|
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
|
||||||
|
EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
|
||||||
|
EventType.FEEDBACK -> listOf("type", "target_event_id")
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,94 +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.prune
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.work.Worker
|
|
||||||
import androidx.work.WorkerParameters
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.di.MatrixKoinComponent
|
|
||||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
|
||||||
import io.realm.Realm
|
|
||||||
import org.koin.standalone.inject
|
|
||||||
|
|
||||||
internal class PruneEventWorker(context: Context,
|
|
||||||
workerParameters: WorkerParameters
|
|
||||||
) : Worker(context, workerParameters), MatrixKoinComponent {
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal class Params(
|
|
||||||
val eventIdsToRedact: List<String>
|
|
||||||
)
|
|
||||||
|
|
||||||
private val monarchy by inject<Monarchy>()
|
|
||||||
|
|
||||||
override fun doWork(): Result {
|
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
|
||||||
?: return Result.failure()
|
|
||||||
|
|
||||||
val result = monarchy.tryTransactionSync { realm ->
|
|
||||||
params.eventIdsToRedact.forEach { eventId ->
|
|
||||||
pruneEvent(realm, eventId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.fold({ Result.retry() }, { Result.success() })
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pruneEvent(realm: Realm, eventIdToRedact: String) {
|
|
||||||
if (eventIdToRedact.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val eventToPrune = EventEntity.where(realm, eventId = eventIdToRedact).findFirst()
|
|
||||||
?: return
|
|
||||||
|
|
||||||
val allowedKeys = computeAllowedKeys(eventToPrune.type)
|
|
||||||
if (allowedKeys.isNotEmpty()) {
|
|
||||||
val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
|
|
||||||
eventToPrune.content = ContentMapper.map(prunedContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun computeAllowedKeys(type: String): List<String> {
|
|
||||||
// Add filtered content, allowed keys in content depends on the event type
|
|
||||||
return when (type) {
|
|
||||||
EventType.STATE_ROOM_MEMBER -> listOf("membership")
|
|
||||||
EventType.STATE_ROOM_CREATE -> listOf("creator")
|
|
||||||
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
|
|
||||||
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
|
|
||||||
"users_default",
|
|
||||||
"events",
|
|
||||||
"events_default",
|
|
||||||
"state_default",
|
|
||||||
"ban",
|
|
||||||
"kick",
|
|
||||||
"redact",
|
|
||||||
"invite")
|
|
||||||
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
|
|
||||||
EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
|
|
||||||
EventType.FEEDBACK -> listOf("type", "target_event_id")
|
|
||||||
else -> emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -16,13 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.send
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.*
|
||||||
import androidx.work.Constraints
|
|
||||||
import androidx.work.ExistingWorkPolicy
|
|
||||||
import androidx.work.NetworkType
|
|
||||||
import androidx.work.OneTimeWorkRequest
|
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
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.Event
|
||||||
@ -74,16 +68,14 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
return cancelableBag
|
return cancelableBag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun redactEvent(event: Event, reason: String?): Cancelable {
|
||||||
override fun sendReaction(reaction: String, targetEventId: String) : Cancelable {
|
//TODO manage local echo ?
|
||||||
val event = eventFactory.createReactionEvent(roomId,targetEventId,reaction).also {
|
//TODO manage media/attachements?
|
||||||
saveLocalEcho(it)
|
val redactWork = createRedactEventWork(event, reason)
|
||||||
}
|
|
||||||
val sendRelationWork = createSendRelationWork(event)
|
|
||||||
WorkManager.getInstance()
|
WorkManager.getInstance()
|
||||||
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendRelationWork)
|
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, redactWork)
|
||||||
.enqueue()
|
.enqueue()
|
||||||
return CancelableWork(sendRelationWork.id)
|
return CancelableWork(redactWork.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
||||||
@ -128,15 +120,17 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSendRelationWork(event: Event): OneTimeWorkRequest {
|
private fun createRedactEventWork(event: Event, reason: String?): 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>()
|
//TODO create local echo of m.room.redaction event?
|
||||||
|
|
||||||
|
val sendContentWorkerParams = RedactEventWorker.Params(
|
||||||
|
roomId, event.eventId!!, reason)
|
||||||
|
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
|
return OneTimeWorkRequestBuilder<RedactEventWorker>()
|
||||||
.setConstraints(WORK_CONSTRAINTS)
|
.setConstraints(WORK_CONSTRAINTS)
|
||||||
.setInputData(sendWorkData)
|
.setInputData(redactWorkData)
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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.send
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
|
import im.vector.matrix.android.internal.di.MatrixKoinComponent
|
||||||
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
|
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||||
|
import org.koin.standalone.inject
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal class RedactEventWorker(context: Context, params: WorkerParameters)
|
||||||
|
: Worker(context, params), MatrixKoinComponent {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class Params(
|
||||||
|
val roomId: String,
|
||||||
|
val eventId: String,
|
||||||
|
val reason: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
private val roomAPI by inject<RoomAPI>()
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
val params = WorkerParamsFactory.fromData<RedactEventWorker.Params>(inputData)
|
||||||
|
?: return Result.failure()
|
||||||
|
|
||||||
|
if (params.eventId == null) {
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
val txID = UUID.randomUUID().toString()
|
||||||
|
|
||||||
|
val result = executeRequest<SendResponse> {
|
||||||
|
apiCall = roomAPI.redactEvent(
|
||||||
|
txID,
|
||||||
|
params.roomId,
|
||||||
|
params.eventId,
|
||||||
|
if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result.fold({
|
||||||
|
when (it) {
|
||||||
|
is Failure.NetworkConnection -> Result.retry()
|
||||||
|
else -> Result.failure()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
Result.success()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -92,7 +92,7 @@ internal class DefaultTimeline(
|
|||||||
|
|
||||||
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>
|
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>
|
||||||
|
|
||||||
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
|
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { results, changeSet ->
|
||||||
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
|
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
|
||||||
handleInitialLoad()
|
handleInitialLoad()
|
||||||
} else {
|
} else {
|
||||||
@ -122,8 +122,22 @@ internal class DefaultTimeline(
|
|||||||
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
|
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
|
||||||
postSnapshot()
|
postSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasChanged = false
|
||||||
|
changeSet.changes.forEach {index ->
|
||||||
|
val eventEntity = results[index]
|
||||||
|
eventEntity?.eventId?.let { eventId ->
|
||||||
|
builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||||
|
//Update the relation of existing event
|
||||||
|
builtEvents[builtIndex]?.let { te ->
|
||||||
|
builtEvents[builtIndex] = timelineEventFactory.create(eventEntity)
|
||||||
|
hasChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasChanged) postSnapshot()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,18 +157,6 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
changeSet.deletions?.forEach {
|
|
||||||
val eventRelations = collection[it]
|
|
||||||
if (eventRelations != null) {
|
|
||||||
builtEventsIdMap[eventRelations.eventId]?.let { builtIndex ->
|
|
||||||
//Update the relation of existing event
|
|
||||||
builtEvents[builtIndex]?.let { te ->
|
|
||||||
builtEvents[builtIndex] = te.copy(annotations = null)
|
|
||||||
hasChange = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasChange)
|
if (hasChange)
|
||||||
postSnapshot()
|
postSnapshot()
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,11 @@ sealed class 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 UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
|
||||||
|
data class UpdateQuickReactAction(val targetEventId: String,val selectedReaction: String,val opposite: String) : RoomDetailActions()
|
||||||
object AcceptInvite : RoomDetailActions()
|
object AcceptInvite : RoomDetailActions()
|
||||||
object RejectInvite : RoomDetailActions()
|
object RejectInvite : RoomDetailActions()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -54,11 +54,7 @@ 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.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
|
||||||
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.api.session.user.model.User
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
@ -69,15 +65,7 @@ import im.vector.riotredesign.core.extensions.observeEvent
|
|||||||
import im.vector.riotredesign.core.glide.GlideApp
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotredesign.core.utils.LiveEvent
|
import im.vector.riotredesign.core.utils.*
|
||||||
import im.vector.riotredesign.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
|
||||||
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
|
|
||||||
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA
|
|
||||||
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA
|
|
||||||
import im.vector.riotredesign.core.utils.checkPermissions
|
|
||||||
import im.vector.riotredesign.core.utils.copyToClipboard
|
|
||||||
import im.vector.riotredesign.core.utils.openCamera
|
|
||||||
import im.vector.riotredesign.core.utils.shareMedia
|
|
||||||
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
|
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
|
||||||
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
|
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
|
||||||
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
|
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
|
||||||
@ -521,7 +509,8 @@ class RoomDetailFragment :
|
|||||||
//we should test the current real state of reaction on this event
|
//we should test the current real state of reaction on this event
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId))
|
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId))
|
||||||
} else {
|
} else {
|
||||||
//TODO it's an undo :/
|
//I need to redact a reaction
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,6 +535,10 @@ class RoomDetailFragment :
|
|||||||
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
|
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
|
||||||
snack.show()
|
snack.show()
|
||||||
}
|
}
|
||||||
|
MessageMenuViewModel.ACTION_DELETE -> {
|
||||||
|
val eventId = actionData.data?.toString() ?: return
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.RedactAction(eventId, context?.getString(R.string.event_redacted_by_user_reason)))
|
||||||
|
}
|
||||||
MessageMenuViewModel.ACTION_SHARE -> {
|
MessageMenuViewModel.ACTION_SHARE -> {
|
||||||
//TODO current data communication is too limited
|
//TODO current data communication is too limited
|
||||||
//Need to now the media type
|
//Need to now the media type
|
||||||
@ -590,8 +583,9 @@ class RoomDetailFragment :
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
MessageMenuViewModel.ACTION_QUICK_REACT -> {
|
MessageMenuViewModel.ACTION_QUICK_REACT -> {
|
||||||
(actionData.data as? Pair<String, String>)?.let { pairData ->
|
//eventId,ClickedOn,Opposite
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(pairData.second, pairData.first))
|
(actionData.data as? Triple<String, String, String>)?.let { (eventId, clickedOn, opposite) ->
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -80,9 +80,13 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
is RoomDetailActions.SendReaction -> handleSendReaction(action)
|
is RoomDetailActions.SendReaction -> handleSendReaction(action)
|
||||||
is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
|
is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
|
||||||
is RoomDetailActions.RejectInvite -> handleRejectInvite()
|
is RoomDetailActions.RejectInvite -> handleRejectInvite()
|
||||||
|
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
|
||||||
|
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
|
||||||
|
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>()
|
private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>()
|
||||||
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
|
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
|
||||||
get() = _sendMessageResultLiveData
|
get() = _sendMessageResultLiveData
|
||||||
@ -190,6 +194,21 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
room.sendReaction(action.reaction, action.targetEventId)
|
room.sendReaction(action.reaction, action.targetEventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleRedactEvent(action: RoomDetailActions.RedactAction) {
|
||||||
|
val event = room.getTimeLineEvent(action.targetEventId) ?: return
|
||||||
|
room.redactEvent(event.root, action.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUndoReact(action: RoomDetailActions.UndoReaction) {
|
||||||
|
room.undoReaction(action.key, action.targetEventId, session.sessionParams.credentials.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
|
||||||
|
room.updateQuickReaction(action.selectedReaction, action.opposite, action.targetEventId, session.sessionParams.credentials.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
||||||
val attachments = action.mediaFiles.map {
|
val attachments = action.mediaFiles.map {
|
||||||
ContentAttachmentData(
|
ContentAttachmentData(
|
||||||
@ -227,6 +246,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
room.join(object : MatrixCallback<Unit> {})
|
room.join(object : MatrixCallback<Unit> {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun observeEventDisplayedActions() {
|
private fun observeEventDisplayedActions() {
|
||||||
// We are buffering scroll events for one second
|
// We are buffering scroll events for one second
|
||||||
// and keep the most recent one to set the read receipt on.
|
// and keep the most recent one to set the read receipt on.
|
||||||
|
@ -95,13 +95,8 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
|
|||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
|
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
|
||||||
override fun didQuickReactWith(clikedOn: String, reactions: List<String>, eventId: String) {
|
override fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List<String>, eventId: String) {
|
||||||
if (reactions.contains(clikedOn)) {
|
actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Triple(eventId, clikedOn, opposite))
|
||||||
//it's an add
|
|
||||||
actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Pair(eventId,clikedOn))
|
|
||||||
} else {
|
|
||||||
//it's a remove
|
|
||||||
}
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,10 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
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 (canRedact(event, currentSession.sessionParams.credentials.userId)) {
|
||||||
|
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_material_delete, event.root.eventId))
|
||||||
|
}
|
||||||
|
|
||||||
if (canQuote(event, messageContent)) {
|
if (canQuote(event, messageContent)) {
|
||||||
//TODO quote icon
|
//TODO quote icon
|
||||||
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))
|
||||||
@ -148,6 +152,14 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun canRedact(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
|
||||||
|
return event.root.sender == myUserId
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun canCopy(type: String): Boolean {
|
private fun canCopy(type: String): Boolean {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
MessageType.MSGTYPE_TEXT,
|
MessageType.MSGTYPE_TEXT,
|
||||||
|
@ -119,12 +119,14 @@ class QuickReactionFragment : BaseMvRxFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (it.selectionResult != null) {
|
if (it.selectionResult != null) {
|
||||||
interactionListener?.didQuickReactWith(it.selectionResult.first, it.selectionResult.second, it.eventId)
|
val clikedOn = it.selectionResult.first
|
||||||
|
interactionListener?.didQuickReactWith(clikedOn, QuickReactionViewModel.getOpposite(clikedOn)
|
||||||
|
?: "", it.selectionResult.second, it.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InteractionListener {
|
interface InteractionListener {
|
||||||
fun didQuickReactWith(clikedOn: String, reactions: List<String>, eventId: String)
|
fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List<String>, eventId: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -110,6 +110,16 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
|
|||||||
val likePositive = "🙂"
|
val likePositive = "🙂"
|
||||||
val likeNegative = "😔"
|
val likeNegative = "😔"
|
||||||
|
|
||||||
|
fun getOpposite(reaction: String): String? {
|
||||||
|
return when (reaction) {
|
||||||
|
agreePositive -> agreeNegative
|
||||||
|
agreeNegative -> agreePositive
|
||||||
|
likePositive -> likeNegative
|
||||||
|
likeNegative -> likePositive
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? {
|
override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? {
|
||||||
// Args are accessible from the context.
|
// Args are accessible from the context.
|
||||||
// val foo = vieWModelContext.args<MyArgs>.foo
|
// val foo = vieWModelContext.args<MyArgs>.foo
|
||||||
|
@ -66,7 +66,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
|| nextEvent?.root?.type != EventType.MESSAGE
|
|| nextEvent?.root?.type != EventType.MESSAGE
|
||||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||||
|
|
||||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
|
||||||
val time = timelineDateFormatter.formatMessageHour(date)
|
val time = timelineDateFormatter.formatMessageHour(date)
|
||||||
val avatarUrl = event.senderAvatar
|
val avatarUrl = event.senderAvatar
|
||||||
val memberName = event.senderName ?: event.root.sender ?: ""
|
val memberName = event.senderName ?: event.root.sender ?: ""
|
||||||
@ -84,8 +83,12 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) }
|
orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) }
|
||||||
)
|
)
|
||||||
|
|
||||||
//Test for reactions UX
|
if (event.root.unsignedData?.redactedEvent != null) {
|
||||||
//informationData.orderedReactionList = listOf( Triple("👍",1,false), Triple("👎",2,false))
|
//message is redacted
|
||||||
|
return buildRedactedItem(informationData, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
||||||
|
|
||||||
// val all = event.root.toContent()
|
// val all = event.root.toContent()
|
||||||
// val ev = all.toModel<Event>()
|
// val ev = all.toModel<Event>()
|
||||||
@ -347,6 +350,19 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildRedactedItem(informationData: MessageInformationData, callback: TimelineEventController.Callback?): RedactedMessageItem? {
|
||||||
|
return RedactedMessageItem_()
|
||||||
|
.informationData(informationData)
|
||||||
|
.avatarClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onAvatarClicked(informationData)
|
||||||
|
}))
|
||||||
|
.memberClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onMemberNameClicked(informationData)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {
|
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {
|
||||||
val spannable = SpannableStringBuilder(body)
|
val spannable = SpannableStringBuilder(body)
|
||||||
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
|
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
|
||||||
|
@ -80,17 +80,23 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
holder.timeView.text = informationData.time
|
holder.timeView.text = informationData.time
|
||||||
holder.memberNameView.text = informationData.memberName
|
holder.memberNameView.text = informationData.memberName
|
||||||
AvatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView)
|
AvatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView)
|
||||||
|
holder.view.setOnClickListener(cellClickListener)
|
||||||
|
holder.view.setOnLongClickListener(longClickListener)
|
||||||
|
holder.avatarImageView.setOnLongClickListener(longClickListener)
|
||||||
|
holder.memberNameView.setOnLongClickListener(longClickListener)
|
||||||
} else {
|
} else {
|
||||||
holder.avatarImageView.setOnClickListener(null)
|
holder.avatarImageView.setOnClickListener(null)
|
||||||
holder.memberNameView.setOnClickListener(null)
|
holder.memberNameView.setOnClickListener(null)
|
||||||
holder.avatarImageView.visibility = View.GONE
|
holder.avatarImageView.visibility = View.GONE
|
||||||
holder.memberNameView.visibility = View.GONE
|
holder.memberNameView.visibility = View.GONE
|
||||||
holder.timeView.visibility = View.GONE
|
holder.timeView.visibility = View.GONE
|
||||||
|
holder.view.setOnClickListener(null)
|
||||||
|
holder.view.setOnLongClickListener(null)
|
||||||
|
holder.avatarImageView.setOnLongClickListener(null)
|
||||||
|
holder.memberNameView.setOnLongClickListener(null)
|
||||||
}
|
}
|
||||||
holder.view.setOnClickListener(cellClickListener)
|
|
||||||
holder.view.setOnLongClickListener(longClickListener)
|
|
||||||
|
|
||||||
if (informationData.orderedReactionList.isNullOrEmpty()) {
|
if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) {
|
||||||
holder.reactionWrapper?.isVisible = false
|
holder.reactionWrapper?.isVisible = false
|
||||||
} else {
|
} else {
|
||||||
//inflate if needed
|
//inflate if needed
|
||||||
@ -119,12 +125,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
|
||||||
holder.reactionFlowHelper?.requestLayout()
|
holder.reactionFlowHelper?.requestLayout()
|
||||||
}
|
}
|
||||||
|
holder.reactionWrapper?.setOnLongClickListener(longClickListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: H) {
|
open fun shouldShowReactionAtBottom() : Boolean {
|
||||||
super.unbind(holder)
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun View.renderSendState() {
|
protected fun View.renderSendState() {
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
|
abstract class RedactedMessageItem : AbsMessageItem<RedactedMessageItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
override lateinit var informationData: MessageInformationData
|
||||||
|
|
||||||
|
override fun getStubType(): Int = STUB_ID
|
||||||
|
|
||||||
|
override fun shouldShowReactionAtBottom() = false
|
||||||
|
|
||||||
|
class Holder : AbsMessageItem.Holder() {
|
||||||
|
override fun getStubId(): Int = STUB_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val STUB_ID = R.id.messageContentRedactedStub
|
||||||
|
}
|
||||||
|
}
|
5
vector/src/main/res/drawable/redacted_background.xml
Normal file
5
vector/src/main/res/drawable/redacted_background.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
<solid android:color="?vctr_list_divider_color" />
|
||||||
|
</shape>
|
@ -15,6 +15,7 @@
|
|||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
|
android:textColor="@color/black"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:autoSizeTextType="uniform"
|
app:autoSizeTextType="uniform"
|
||||||
app:layout_constraintBottom_toTopOf="@id/quick_react_agree_text"
|
app:layout_constraintBottom_toTopOf="@id/quick_react_agree_text"
|
||||||
@ -34,10 +35,10 @@
|
|||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
|
android:textColor="@color/black"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:autoSizeTextType="uniform"
|
app:autoSizeTextType="uniform"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/quick_react_1_text"
|
app:layout_constraintBottom_toBottomOf="@id/quick_react_1_text"
|
||||||
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/center_guideline"
|
app:layout_constraintEnd_toStartOf="@id/center_guideline"
|
||||||
app:layout_constraintStart_toEndOf="@id/quick_react_1_text"
|
app:layout_constraintStart_toEndOf="@id/quick_react_1_text"
|
||||||
app:layout_constraintTop_toTopOf="@id/quick_react_1_text"
|
app:layout_constraintTop_toTopOf="@id/quick_react_1_text"
|
||||||
@ -73,6 +74,7 @@
|
|||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
|
android:textColor="@color/black"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:autoSizeTextType="uniform"
|
app:autoSizeTextType="uniform"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/quick_react_1_text"
|
app:layout_constraintBottom_toBottomOf="@+id/quick_react_1_text"
|
||||||
@ -91,6 +93,7 @@
|
|||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
|
android:textColor="@color/black"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:autoSizeTextType="uniform"
|
app:autoSizeTextType="uniform"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/quick_react_3_text"
|
app:layout_constraintBottom_toBottomOf="@id/quick_react_3_text"
|
||||||
|
@ -81,21 +81,29 @@
|
|||||||
android:layout="@layout/item_timeline_event_file_stub"
|
android:layout="@layout/item_timeline_event_file_stub"
|
||||||
tools:ignore="MissingConstraints" />
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/messageContentRedactedStub"
|
||||||
|
style="@style/TimelineContentStubLayoutParams"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginEnd="56dp"
|
||||||
|
android:layout_marginRight="56dp"
|
||||||
|
android:layout="@layout/item_timeline_event_redacted_stub"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
|
||||||
<!-- TODO: For now we show 8 reactions maximum, this will need rework when needed-->
|
<!-- TODO: For now we show 8 reactions maximum, this will need rework when needed-->
|
||||||
<ViewStub
|
<ViewStub
|
||||||
android:id="@+id/messageBottomInfo"
|
android:id="@+id/messageBottomInfo"
|
||||||
android:inflatedId="@+id/messageBottomInfo"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:inflatedId="@+id/messageBottomInfo"
|
||||||
android:layout="@layout/item_timeline_event_bottom_reactions_stub"
|
android:layout="@layout/item_timeline_event_bottom_reactions_stub"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
|
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
android:layout_marginBottom="4dp"
|
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
</ViewStub>
|
</ViewStub>
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:background="@drawable/redacted_background" />
|
@ -2,9 +2,9 @@
|
|||||||
<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:clipChildren="false"
|
|
||||||
android:layout_width="44dp"
|
android:layout_width="44dp"
|
||||||
android:layout_height="26dp">
|
android:layout_height="26dp"
|
||||||
|
android:clipChildren="false">
|
||||||
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@ -16,21 +16,21 @@
|
|||||||
<im.vector.riotredesign.features.reactions.widget.DotsView
|
<im.vector.riotredesign.features.reactions.widget.DotsView
|
||||||
android:id="@+id/dots"
|
android:id="@+id/dots"
|
||||||
android:layout_width="30dp"
|
android:layout_width="30dp"
|
||||||
android:clipChildren="false"
|
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
|
android:clipChildren="false"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/reactionText"
|
app:layout_constraintBottom_toBottomOf="@+id/reactionText"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/reactionText"
|
||||||
app:layout_constraintStart_toStartOf="@+id/reactionText"
|
app:layout_constraintStart_toStartOf="@+id/reactionText"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reactionText"
|
app:layout_constraintTop_toTopOf="@+id/reactionText" />
|
||||||
app:layout_constraintEnd_toEndOf="@+id/reactionText"/>
|
|
||||||
|
|
||||||
<im.vector.riotredesign.features.reactions.widget.CircleView
|
<im.vector.riotredesign.features.reactions.widget.CircleView
|
||||||
android:id="@+id/circle"
|
android:id="@+id/circle"
|
||||||
android:layout_width="14dp"
|
android:layout_width="14dp"
|
||||||
android:layout_height="14dp"
|
android:layout_height="14dp"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/reactionText"
|
app:layout_constraintBottom_toBottomOf="@+id/reactionText"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/reactionText"
|
||||||
app:layout_constraintStart_toStartOf="@+id/reactionText"
|
app:layout_constraintStart_toStartOf="@+id/reactionText"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reactionText"
|
app:layout_constraintTop_toTopOf="@+id/reactionText" />
|
||||||
app:layout_constraintEnd_toEndOf="@+id/reactionText"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/reactionText"
|
android:id="@+id/reactionText"
|
||||||
@ -40,11 +40,12 @@
|
|||||||
android:layout_marginStart="6dp"
|
android:layout_marginStart="6dp"
|
||||||
android:layout_marginLeft="6dp"
|
android:layout_marginLeft="6dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
tools:text="👍"
|
android:textColor="@color/black"
|
||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
tools:text="👍" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/reactionCount"
|
android:id="@+id/reactionCount"
|
||||||
|
@ -13,4 +13,7 @@
|
|||||||
<string name="reactions_like">Like</string>
|
<string name="reactions_like">Like</string>
|
||||||
<string name="message_add_reaction">Add Reaction</string>
|
<string name="message_add_reaction">Add Reaction</string>
|
||||||
|
|
||||||
|
<string name="event_redacted_by_user_reason">Event deleted by user</string>
|
||||||
|
<string name="event_redacted_by_admin_reason">Event moderated by room admin</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue
Block a user