Support both unstable and stable prefixes.

Author: Onuray
This commit is contained in:
Benoit Marty 2022-03-09 10:40:29 +01:00
parent 2048b859c5
commit 20c1886fed
31 changed files with 164 additions and 128 deletions

1
changelog.d/5340.bugfix Normal file
View File

@ -0,0 +1 @@
Support both stable and unstable prefixes

View File

@ -349,7 +349,7 @@ fun Event.isAttachmentMessage(): Boolean {
} }
} }
fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
@ -372,7 +372,7 @@ fun Event.getRelationContent(): RelationDefaultContent? {
* Returns the poll question or null otherwise * Returns the poll question or null otherwise
*/ */
fun Event.getPollQuestion(): String? = fun Event.getPollQuestion(): String? =
getPollContent()?.pollCreationInfo?.question?.question getPollContent()?.getBestPollCreationInfo()?.question?.getBestQuestion()
/** /**
* Returns the relation content for a specific type or null otherwise * Returns the relation content for a specific type or null otherwise

View File

@ -103,9 +103,9 @@ object EventType {
const val REACTION = "m.reaction" const val REACTION = "m.reaction"
// Poll // Poll
const val POLL_START = "org.matrix.msc3381.poll.start" val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start")
const val POLL_RESPONSE = "org.matrix.msc3381.poll.response" val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response")
const val POLL_END = "org.matrix.msc3381.poll.end" val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end")
// Unwedging // Unwedging
internal const val DUMMY = "m.dummy" internal const val DUMMY = "m.dummy"

View File

@ -39,37 +39,46 @@ data class MessageLocationContent(
*/ */
@Json(name = "geo_uri") val geoUri: String, @Json(name = "geo_uri") val geoUri: String,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
/** /**
* See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md * See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
*/ */
@Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null, @Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
@Json(name = "m.location") val locationInfo: LocationInfo? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, /**
@Json(name = "m.new_content") override val newContent: Content? = null, * Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
*/
@Json(name = "org.matrix.msc3488.ts") val unstableTs: Long? = null,
@Json(name = "m.ts") val ts: Long? = null,
@Json(name = "org.matrix.msc1767.text") val unstableText: String? = null,
@Json(name = "m.text") val text: String? = null,
/** /**
* m.asset defines a generic asset that can be used for location tracking but also in other places like * m.asset defines a generic asset that can be used for location tracking but also in other places like
* inventories, geofencing, checkins/checkouts etc. * inventories, geofencing, checkins/checkouts etc.
* It should contain a mandatory namespaced type key defining what particular asset is being referred to. * It should contain a mandatory namespaced type key defining what particular asset is being referred to.
* For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid. * For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
*/ */
@Json(name = "m.asset") val locationAsset: LocationAsset? = null, @Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
@Json(name = "m.asset") val locationAsset: LocationAsset? = null
/**
* Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
*/
@Json(name = "org.matrix.msc3488.ts") val ts: Long? = null,
@Json(name = "org.matrix.msc1767.text") val text: String? = null
) : MessageContent { ) : MessageContent {
fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri fun getBestLocationInfo() = locationInfo ?: unstableLocationInfo
fun getBestTs() = ts ?: unstableTs
fun getBestText() = text ?: unstableText
fun getBestLocationAsset() = locationAsset ?: unstableLocationAsset
fun getBestGeoUri() = getBestLocationInfo()?.geoUri ?: geoUri
/** /**
* @return true if the location asset is a user location, not a generic one. * @return true if the location asset is a user location, not a generic one.
*/ */
fun isSelfLocation(): Boolean { fun isSelfLocation(): Boolean {
// Should behave like m.self if locationAsset is null // Should behave like m.self if locationAsset is null
val locationAsset = getBestLocationAsset()
return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF
} }
} }

View File

@ -31,5 +31,9 @@ data class MessagePollContent(
@Json(name = "body") override val body: String = "", @Json(name = "body") override val body: String = "",
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null, @Json(name = "m.new_content") override val newContent: Content? = null,
@Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null @Json(name = "org.matrix.msc3381.poll.start") val unstablePollCreationInfo: PollCreationInfo? = null,
) : MessageContent @Json(name = "m.poll.start") val pollCreationInfo: PollCreationInfo? = null
) : MessageContent {
fun getBestPollCreationInfo() = pollCreationInfo ?: unstablePollCreationInfo
}

View File

@ -31,5 +31,9 @@ data class MessagePollResponseContent(
@Json(name = "body") override val body: String = "", @Json(name = "body") override val body: String = "",
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null, @Json(name = "m.new_content") override val newContent: Content? = null,
@Json(name = "org.matrix.msc3381.poll.response") val response: PollResponse? = null @Json(name = "org.matrix.msc3381.poll.response") val unstableResponse: PollResponse? = null,
) : MessageContent @Json(name = "m.response") val response: PollResponse? = null
) : MessageContent {
fun getBestResponse() = response ?: unstableResponse
}

View File

@ -22,5 +22,9 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PollAnswer( data class PollAnswer(
@Json(name = "id") val id: String? = null, @Json(name = "id") val id: String? = null,
@Json(name = "org.matrix.msc1767.text") val answer: String? = null @Json(name = "org.matrix.msc1767.text") val unstableAnswer: String? = null,
) @Json(name = "m.text") val answer: String? = null
) {
fun getBestAnswer() = answer ?: unstableAnswer
}

View File

@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PollCreationInfo( data class PollCreationInfo(
@Json(name = "question") val question: PollQuestion? = null, @Json(name = "question") val question: PollQuestion? = null,
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED, @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
@Json(name = "max_selections") val maxSelections: Int = 1, @Json(name = "max_selections") val maxSelections: Int = 1,
@Json(name = "answers") val answers: List<PollAnswer>? = null @Json(name = "answers") val answers: List<PollAnswer>? = null
) )

View File

@ -21,5 +21,9 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PollQuestion( data class PollQuestion(
@Json(name = "org.matrix.msc1767.text") val question: String? = null @Json(name = "org.matrix.msc1767.text") val unstableQuestion: String? = null,
) @Json(name = "m.text") val question: String? = null
) {
fun getBestQuestion() = question ?: unstableQuestion
}

View File

@ -25,11 +25,17 @@ enum class PollType {
* Voters should see results as soon as they have voted. * Voters should see results as soon as they have voted.
*/ */
@Json(name = "org.matrix.msc3381.poll.disclosed") @Json(name = "org.matrix.msc3381.poll.disclosed")
DISCLOSED_UNSTABLE,
@Json(name = "m.poll.disclosed")
DISCLOSED, DISCLOSED,
/** /**
* Results should be only revealed when the poll is ended. * Results should be only revealed when the poll is ended.
*/ */
@Json(name = "org.matrix.msc3381.poll.undisclosed") @Json(name = "org.matrix.msc3381.poll.undisclosed")
UNDISCLOSED_UNSTABLE,
@Json(name = "m.poll.undisclosed")
UNDISCLOSED UNDISCLOSED
} }

View File

@ -32,7 +32,6 @@ object RoomSummaryConstants {
EventType.CALL_ANSWER, EventType.CALL_ANSWER,
EventType.ENCRYPTED, EventType.ENCRYPTED,
EventType.STICKER, EventType.STICKER,
EventType.REACTION, EventType.REACTION
EventType.POLL_START ) + EventType.POLL_START
)
} }

View File

@ -135,7 +135,7 @@ fun TimelineEvent.getEditedEventId(): String? {
fun TimelineEvent.getLastMessageContent(): MessageContent? { fun TimelineEvent.getLastMessageContent(): MessageContent? {
return when (root.getClearType()) { return when (root.getClearType()) {
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>() EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>() in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
} }
} }

View File

@ -56,7 +56,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
val allEvents = (newJoinEvents + inviteEvents).filter { event -> val allEvents = (newJoinEvents + inviteEvents).filter { event ->
when (event.type) { when (event.type) {
EventType.POLL_START, in EventType.POLL_START,
EventType.MESSAGE, EventType.MESSAGE,
EventType.REDACTION, EventType.REDACTION,
EventType.ENCRYPTED, EventType.ENCRYPTED,

View File

@ -86,11 +86,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
// TODO Add ? // TODO Add ?
// EventType.KEY_VERIFICATION_READY, // EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.ENCRYPTED, EventType.ENCRYPTED
EventType.POLL_START, ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END
EventType.POLL_RESPONSE,
EventType.POLL_END
)
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
return allowedTypes.contains(eventType) return allowedTypes.contains(eventType)
@ -156,7 +153,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace! // A replace!
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
} else if (event.getClearType() == EventType.POLL_RESPONSE) { } else if (event.getClearType() in EventType.POLL_RESPONSE) {
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent -> event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent ->
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
@ -177,12 +174,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
handleVerification(realm, event, roomId, isLocalEcho, it) handleVerification(realm, event, roomId, isLocalEcho, it)
} }
} }
EventType.POLL_RESPONSE -> { in EventType.POLL_RESPONSE -> {
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId) handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId)
} }
} }
EventType.POLL_END -> { in EventType.POLL_END -> {
event.content.toModel<MessageEndPollContent>(catchError = true)?.let { event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
handleEndPoll(realm, event, it, roomId, isLocalEcho) handleEndPoll(realm, event, it, roomId, isLocalEcho)
} }
@ -217,7 +214,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
} }
} }
} }
EventType.POLL_START -> { in EventType.POLL_START -> {
val content: MessagePollContent? = event.content.toModel() val content: MessagePollContent? = event.content.toModel()
if (content?.relatesTo?.type == RelationType.REPLACE) { if (content?.relatesTo?.type == RelationType.REPLACE) {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
@ -225,12 +222,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
handleReplace(realm, event, content, roomId, isLocalEcho) handleReplace(realm, event, content, roomId, isLocalEcho)
} }
} }
EventType.POLL_RESPONSE -> { in EventType.POLL_RESPONSE -> {
event.content.toModel<MessagePollResponseContent>(catchError = true)?.let { event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
handleResponse(realm, event, it, roomId, isLocalEcho) handleResponse(realm, event, it, roomId, isLocalEcho)
} }
} }
EventType.POLL_END -> { in EventType.POLL_END -> {
event.content.toModel<MessageEndPollContent>(catchError = true)?.let { event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
handleEndPoll(realm, event, it, roomId, isLocalEcho) handleEndPoll(realm, event, it, roomId, isLocalEcho)
} }
@ -407,12 +404,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
return return
} }
val option = content.response?.answers?.first() ?: return Unit.also { val option = content.getBestResponse()?.answers?.first() ?: return Unit.also {
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}") Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
} }
// Check if this option is in available options // Check if this option is in available options
if (!targetPollContent.pollCreationInfo?.answers?.map { it.id }?.contains(option).orFalse()) { if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(option).orFalse()) {
Timber.v("## POLL $targetEventId doesn't contain option $option") Timber.v("## POLL $targetEventId doesn't contain option $option")
return return
} }

View File

@ -71,7 +71,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
when (typeToPrune) { when (typeToPrune) {
EventType.ENCRYPTED, EventType.ENCRYPTED,
EventType.MESSAGE, EventType.MESSAGE,
EventType.POLL_START -> { in EventType.POLL_START -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}") Timber.d("REDACTION for message ${eventToPrune.eventId}")
val unsignedData = EventMapper.map(eventToPrune).unsignedData val unsignedData = EventMapper.map(eventToPrune).unsignedData
?: UnsignedData(null, null) ?: UnsignedData(null, null)

View File

@ -137,16 +137,11 @@ internal class LocalEchoEventFactory @Inject constructor(
options: List<String>, options: List<String>,
pollType: PollType): MessagePollContent { pollType: PollType): MessagePollContent {
return MessagePollContent( return MessagePollContent(
pollCreationInfo = PollCreationInfo( unstablePollCreationInfo = PollCreationInfo(
question = PollQuestion( question = PollQuestion(unstableQuestion = question),
question = question
),
kind = pollType, kind = pollType,
answers = options.map { option -> answers = options.map { option ->
PollAnswer( PollAnswer(id = UUID.randomUUID().toString(), unstableAnswer = option)
id = UUID.randomUUID().toString(),
answer = option
)
} }
) )
) )
@ -167,7 +162,7 @@ internal class LocalEchoEventFactory @Inject constructor(
originServerTs = dummyOriginServerTs(), originServerTs = dummyOriginServerTs(),
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = EventType.POLL_START, type = EventType.POLL_START.first(),
content = newContent.toContent() content = newContent.toContent()
) )
} }
@ -179,11 +174,9 @@ internal class LocalEchoEventFactory @Inject constructor(
body = answerId, body = answerId,
relatesTo = RelationDefaultContent( relatesTo = RelationDefaultContent(
type = RelationType.REFERENCE, type = RelationType.REFERENCE,
eventId = pollEventId), eventId = pollEventId
response = PollResponse( ),
answers = listOf(answerId) unstableResponse = PollResponse(answers = listOf(answerId))
)
) )
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
return Event( return Event(
@ -191,7 +184,7 @@ internal class LocalEchoEventFactory @Inject constructor(
originServerTs = dummyOriginServerTs(), originServerTs = dummyOriginServerTs(),
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = EventType.POLL_RESPONSE, type = EventType.POLL_RESPONSE.first(),
content = content.toContent(), content = content.toContent(),
unsignedData = UnsignedData(age = null, transactionId = localId)) unsignedData = UnsignedData(age = null, transactionId = localId))
} }
@ -207,7 +200,7 @@ internal class LocalEchoEventFactory @Inject constructor(
originServerTs = dummyOriginServerTs(), originServerTs = dummyOriginServerTs(),
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = EventType.POLL_START, type = EventType.POLL_START.first(),
content = content.toContent(), content = content.toContent(),
unsignedData = UnsignedData(age = null, transactionId = localId)) unsignedData = UnsignedData(age = null, transactionId = localId))
} }
@ -226,7 +219,7 @@ internal class LocalEchoEventFactory @Inject constructor(
originServerTs = dummyOriginServerTs(), originServerTs = dummyOriginServerTs(),
senderId = userId, senderId = userId,
eventId = localId, eventId = localId,
type = EventType.POLL_END, type = EventType.POLL_END.first(),
content = content.toContent(), content = content.toContent(),
unsignedData = UnsignedData(age = null, transactionId = localId)) unsignedData = UnsignedData(age = null, transactionId = localId))
} }
@ -239,15 +232,10 @@ internal class LocalEchoEventFactory @Inject constructor(
val content = MessageLocationContent( val content = MessageLocationContent(
geoUri = geoUri, geoUri = geoUri,
body = geoUri, body = geoUri,
locationInfo = LocationInfo( unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
geoUri = geoUri, unstableLocationAsset = LocationAsset(type = LocationAssetType.SELF),
description = geoUri unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
), unstableText = geoUri
locationAsset = LocationAsset(
type = LocationAssetType.SELF
),
ts = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
text = geoUri
) )
return createMessageEvent(roomId, content) return createMessageEvent(roomId, content)
} }
@ -638,7 +626,7 @@ internal class LocalEchoEventFactory @Inject constructor(
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.") MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.") MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.") MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.pollCreationInfo?.question?.question ?: "") MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
else -> return TextContent(content?.body ?: "") else -> return TextContent(content?.body ?: "")
} }
} }

View File

@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
fun TimelineEvent.canReact(): Boolean { fun TimelineEvent.canReact(): Boolean {
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER, EventType.POLL_START) && return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START &&
root.sendState == SendState.SYNCED && root.sendState == SendState.SYNCED &&
!root.isRedacted() !root.isRedacted()
} }

View File

@ -1189,7 +1189,7 @@ class TimelineFragment @Inject constructor(
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong()) val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
getString(R.string.voice_message_reply_content, formattedDuration) getString(R.string.voice_message_reply_content, formattedDuration)
} else if (messageContent is MessagePollContent) { } else if (messageContent is MessagePollContent) {
messageContent.pollCreationInfo?.question?.question messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
} else { } else {
messageContent?.body ?: "" messageContent?.body ?: ""
} }
@ -2165,7 +2165,7 @@ class TimelineFragment @Inject constructor(
timelineViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) timelineViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
} }
is EventSharedAction.Edit -> { is EventSharedAction.Edit -> {
if (action.eventType == EventType.POLL_START) { if (action.eventType in EventType.POLL_START) {
navigator.openCreatePoll(requireContext(), timelineArgs.roomId, action.eventId, PollMode.EDIT) navigator.openCreatePoll(requireContext(), timelineArgs.roomId, action.eventId, PollMode.EDIT)
} else if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) { } else if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))

View File

@ -210,8 +210,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
EventType.CALL_ANSWER -> { EventType.CALL_ANSWER -> {
noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse()) noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
} }
EventType.POLL_START -> { in EventType.POLL_START -> {
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.pollCreationInfo?.question?.question ?: "" timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion()
?: ""
} }
else -> null else -> null
} }
@ -373,7 +374,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
if (canRedact(timelineEvent, actionPermissions)) { if (canRedact(timelineEvent, actionPermissions)) {
if (timelineEvent.root.getClearType() == EventType.POLL_START) { if (timelineEvent.root.getClearType() in EventType.POLL_START) {
add(EventSharedAction.Redact( add(EventSharedAction.Redact(
eventId, eventId,
askForReason = informationData.senderId != session.myUserId, askForReason = informationData.senderId != session.myUserId,
@ -425,7 +426,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
// Only EventType.MESSAGE and EventType.POLL_START event types are supported for the moment // Only EventType.MESSAGE and EventType.POLL_START event types are supported for the moment
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false if (event.root.getClearType() !in EventType.POLL_START + EventType.MESSAGE) return false
if (!actionPermissions.canSendMessage) return false if (!actionPermissions.canSendMessage) return false
return when (messageContent?.msgType) { return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
@ -511,7 +512,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.POLL_START)) return false if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false
// Message sent by the current user can always be redacted // Message sent by the current user can always be redacted
if (event.root.senderId == session.myUserId) return true if (event.root.senderId == session.myUserId) return true
// Check permission for messages sent by other users // Check permission for messages sent by other users
@ -526,13 +527,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun canViewReactions(event: TimelineEvent): Boolean { private fun canViewReactions(event: TimelineEvent): Boolean {
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.POLL_START)) return false if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
} }
private fun canEdit(event: TimelineEvent, myUserId: String, actionPermissions: ActionPermissions): Boolean { private fun canEdit(event: TimelineEvent, myUserId: String, actionPermissions: ActionPermissions): Boolean {
// Only event of type EventType.MESSAGE and EventType.POLL_START are supported for the moment // Only event of type EventType.MESSAGE and EventType.POLL_START are supported for the moment
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false if (event.root.getClearType() !in listOf(EventType.MESSAGE) + EventType.POLL_START) return false
if (!actionPermissions.canSendMessage) return false if (!actionPermissions.canSendMessage) return false
// TODO if user is admin or moderator // TODO if user is admin or moderator
val messageContent = event.root.getClearContent().toModel<MessageContent>() val messageContent = event.root.getClearContent().toModel<MessageContent>()
@ -578,13 +579,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
private fun canEndPoll(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { private fun canEndPoll(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
return event.root.getClearType() == EventType.POLL_START && return event.root.getClearType() in EventType.POLL_START &&
canRedact(event, actionPermissions) && canRedact(event, actionPermissions) &&
event.annotations?.pollResponseSummary?.closedTime == null event.annotations?.pollResponseSummary?.closedTime == null
} }
private fun canEditPoll(event: TimelineEvent): Boolean { private fun canEditPoll(event: TimelineEvent): Boolean {
return event.root.getClearType() == EventType.POLL_START && return event.root.getClearType() in EventType.POLL_START &&
event.annotations?.pollResponseSummary?.closedTime == null && event.annotations?.pollResponseSummary?.closedTime == null &&
event.annotations?.pollResponseSummary?.aggregatedContent?.totalVotes ?: 0 == 0 event.annotations?.pollResponseSummary?.aggregatedContent?.totalVotes ?: 0 == 0
} }

View File

@ -247,7 +247,7 @@ class MessageItemFactory @Inject constructor(
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
val winnerVoteCount = pollResponseSummary?.winnerVoteCount val winnerVoteCount = pollResponseSummary?.winnerVoteCount
val isPollSent = informationData.sendState.isSent() val isPollSent = informationData.sendState.isSent()
val isPollUndisclosed = pollContent.pollCreationInfo?.kind == PollType.UNDISCLOSED val isPollUndisclosed = pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED_UNSTABLE
val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let { val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let {
when { when {
@ -262,13 +262,13 @@ class MessageItemFactory @Inject constructor(
} }
} }
pollContent.pollCreationInfo?.answers?.forEach { option -> pollContent.getBestPollCreationInfo()?.answers?.forEach { option ->
val voteSummary = pollResponseSummary?.votes?.get(option.id) val voteSummary = pollResponseSummary?.votes?.get(option.id)
val isMyVote = pollResponseSummary?.myVote == option.id val isMyVote = pollResponseSummary?.myVote == option.id
val voteCount = voteSummary?.total ?: 0 val voteCount = voteSummary?.total ?: 0
val votePercentage = voteSummary?.percentage ?: 0.0 val votePercentage = voteSummary?.percentage ?: 0.0
val optionId = option.id ?: "" val optionId = option.id ?: ""
val optionAnswer = option.answer ?: "" val optionAnswer = option.getBestAnswer() ?: ""
optionViewStates.add( optionViewStates.add(
if (!isPollSent) { if (!isPollSent) {
@ -291,7 +291,7 @@ class MessageItemFactory @Inject constructor(
) )
} }
val question = pollContent.pollCreationInfo?.question?.question ?: "" val question = pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
return PollItem_() return PollItem_()
.attributes(attributes) .attributes(attributes)

View File

@ -94,7 +94,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
when (event.root.getClearType()) { when (event.root.getClearType()) {
// Message itemsX // Message itemsX
EventType.STICKER, EventType.STICKER,
EventType.POLL_START, in EventType.POLL_START,
EventType.MESSAGE -> messageItemFactory.create(params) EventType.MESSAGE -> messageItemFactory.create(params)
EventType.REDACTION, EventType.REDACTION,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
@ -107,8 +107,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.CALL_SELECT_ANSWER, EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE, EventType.CALL_NEGOTIATE,
EventType.REACTION, EventType.REACTION,
EventType.POLL_RESPONSE, in EventType.POLL_RESPONSE,
EventType.POLL_END -> noticeItemFactory.create(params) in EventType.POLL_END -> noticeItemFactory.create(params)
// Calls // Calls
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,

View File

@ -120,14 +120,14 @@ class DisplayableEventFormatter @Inject constructor(
EventType.CALL_CANDIDATES -> { EventType.CALL_CANDIDATES -> {
span { } span { }
} }
EventType.POLL_START -> { in EventType.POLL_START -> {
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.pollCreationInfo?.question?.question timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion()
?: stringProvider.getString(R.string.sent_a_poll) ?: stringProvider.getString(R.string.sent_a_poll)
} }
EventType.POLL_RESPONSE -> { in EventType.POLL_RESPONSE -> {
stringProvider.getString(R.string.poll_response_room_list_preview) stringProvider.getString(R.string.poll_response_room_list_preview)
} }
EventType.POLL_END -> { in EventType.POLL_END -> {
stringProvider.getString(R.string.poll_end_room_list_preview) stringProvider.getString(R.string.poll_end_room_list_preview)
} }
else -> { else -> {

View File

@ -106,8 +106,8 @@ class NoticeEventFormatter @Inject constructor(
EventType.STATE_SPACE_PARENT, EventType.STATE_SPACE_PARENT,
EventType.REDACTION, EventType.REDACTION,
EventType.STICKER, EventType.STICKER,
EventType.POLL_RESPONSE, in EventType.POLL_RESPONSE,
EventType.POLL_END -> formatDebug(timelineEvent.root) in EventType.POLL_END -> formatDebug(timelineEvent.root)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")
null null

View File

@ -50,9 +50,8 @@ object TimelineDisplayableEvents {
EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_ROOM_JOIN_RULES,
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL
EventType.POLL_START ) + EventType.POLL_START
)
} }
fun TimelineEvent.canBeMerged(): Boolean { fun TimelineEvent.canBeMerged(): Boolean {

View File

@ -43,10 +43,9 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
// Can be rendered in bubbles, other types will fallback to default // Can be rendered in bubbles, other types will fallback to default
private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf( private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf(
EventType.MESSAGE, EventType.MESSAGE,
EventType.POLL_START,
EventType.ENCRYPTED, EventType.ENCRYPTED,
EventType.STICKER EventType.STICKER
) ) + EventType.POLL_START
// Can't be rendered in bubbles, so get back to default layout // Can't be rendered in bubbles, so get back to default layout
private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf( private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf(

View File

@ -60,9 +60,9 @@ class CreatePollController @Inject constructor(
pollTypeChangedListener { _, id -> pollTypeChangedListener { _, id ->
host.callback?.onPollTypeChanged( host.callback?.onPollTypeChanged(
if (id == R.id.openPollTypeRadioButton) { if (id == R.id.openPollTypeRadioButton) {
PollType.DISCLOSED PollType.DISCLOSED_UNSTABLE
} else { } else {
PollType.UNDISCLOSED PollType.UNDISCLOSED_UNSTABLE
} }
) )
} }

View File

@ -71,9 +71,10 @@ class CreatePollViewModel @AssistedInject constructor(
val event = room.getTimelineEvent(eventId) ?: return val event = room.getTimelineEvent(eventId) ?: return
val content = event.getLastMessageContent() as? MessagePollContent ?: return val content = event.getLastMessageContent() as? MessagePollContent ?: return
val pollType = content.pollCreationInfo?.kind ?: PollType.DISCLOSED val pollCreationInfo = content.getBestPollCreationInfo()
val question = content.pollCreationInfo?.question?.question ?: "" val pollType = pollCreationInfo?.kind ?: PollType.DISCLOSED_UNSTABLE
val options = content.pollCreationInfo?.answers?.mapNotNull { it.answer } ?: List(MIN_OPTIONS_COUNT) { "" } val question = pollCreationInfo?.question?.getBestQuestion() ?: ""
val options = pollCreationInfo?.answers?.mapNotNull { it.getBestAnswer() } ?: List(MIN_OPTIONS_COUNT) { "" }
setState { setState {
copy( copy(

View File

@ -27,7 +27,7 @@ data class CreatePollViewState(
val options: List<String> = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" }, val options: List<String> = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" },
val canCreatePoll: Boolean = false, val canCreatePoll: Boolean = false,
val canAddMoreOptions: Boolean = true, val canAddMoreOptions: Boolean = true,
val pollType: PollType = PollType.DISCLOSED val pollType: PollType = PollType.DISCLOSED_UNSTABLE
) : MavericksState { ) : MavericksState {
constructor(args: CreatePollArgs) : this( constructor(args: CreatePollArgs) : this(

View File

@ -28,7 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.message.PollType
abstract class PollTypeSelectionItem : VectorEpoxyModel<PollTypeSelectionItem.Holder>() { abstract class PollTypeSelectionItem : VectorEpoxyModel<PollTypeSelectionItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var pollType: PollType = PollType.DISCLOSED var pollType: PollType = PollType.DISCLOSED_UNSTABLE
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var pollTypeChangedListener: RadioGroup.OnCheckedChangeListener? = null var pollTypeChangedListener: RadioGroup.OnCheckedChangeListener? = null
@ -38,8 +38,8 @@ abstract class PollTypeSelectionItem : VectorEpoxyModel<PollTypeSelectionItem.Ho
holder.pollTypeRadioGroup.check( holder.pollTypeRadioGroup.check(
when (pollType) { when (pollType) {
PollType.DISCLOSED -> R.id.openPollTypeRadioButton PollType.DISCLOSED_UNSTABLE, PollType.DISCLOSED -> R.id.openPollTypeRadioButton
PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton
} }
) )

View File

@ -38,8 +38,16 @@ data class ElementWellKnown(
val riotE2E: E2EWellKnownConfig? = null, val riotE2E: E2EWellKnownConfig? = null,
@Json(name = "org.matrix.msc3488.tile_server") @Json(name = "org.matrix.msc3488.tile_server")
val mapTileServerConfig: MapTileServerConfig? = null val unstableMapTileServerConfig: MapTileServerConfig? = null,
)
@Json(name = "m.tile_server")
val stableMapTileServerConfig: MapTileServerConfig? = null
) {
@Transient
var mapTileServerConfig: MapTileServerConfig? = null
get() = stableMapTileServerConfig ?: unstableMapTileServerConfig
}
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class E2EWellKnownConfig( data class E2EWellKnownConfig(

View File

@ -22,6 +22,7 @@ import org.amshove.kluent.shouldBeTrue
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.room.model.message.LocationAsset import org.matrix.android.sdk.api.session.room.model.message.LocationAsset
import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType
import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
class LocationDataTest { class LocationDataTest {
@ -64,13 +65,24 @@ class LocationDataTest {
@Test @Test
fun selfLocationTest() { fun selfLocationTest() {
val contentWithNullAsset = MessageLocationContent(body = "", geoUri = "", locationAsset = null) val contentWithNullAsset = MessageLocationContent(body = "", geoUri = "")
contentWithNullAsset.isSelfLocation().shouldBeTrue() contentWithNullAsset.isSelfLocation().shouldBeTrue()
val contentWithNullAssetType = MessageLocationContent(body = "", geoUri = "", locationAsset = LocationAsset(type = null)) val contentWithNullAssetType = MessageLocationContent(body = "", geoUri = "", unstableLocationAsset = LocationAsset(type = null))
contentWithNullAssetType.isSelfLocation().shouldBeTrue() contentWithNullAssetType.isSelfLocation().shouldBeTrue()
val contentWithSelfAssetType = MessageLocationContent(body = "", geoUri = "", locationAsset = LocationAsset(type = LocationAssetType.SELF)) val contentWithSelfAssetType = MessageLocationContent(body = "", geoUri = "", unstableLocationAsset = LocationAsset(type = LocationAssetType.SELF))
contentWithSelfAssetType.isSelfLocation().shouldBeTrue() contentWithSelfAssetType.isSelfLocation().shouldBeTrue()
} }
@Test
fun unstablePrefixTest() {
val geoUri = "geo :12.34,56.78;13.56"
val contentWithUnstablePrefixes = MessageLocationContent(body = "", geoUri = "", unstableLocationInfo = LocationInfo(geoUri = geoUri))
contentWithUnstablePrefixes.getBestLocationInfo()?.geoUri.shouldBeEqualTo(geoUri)
val contentWithStablePrefixes = MessageLocationContent(body = "", geoUri = "", locationInfo = LocationInfo(geoUri = geoUri))
contentWithStablePrefixes.getBestLocationInfo()?.geoUri.shouldBeEqualTo(geoUri)
}
} }