Merge branch 'develop' into feature/ons/poll_tests

* develop:
  Fix lint error.
  Fix lint error.
  Avoid stable prefix
  Support both unstable and stable prefixes.
This commit is contained in:
Onuray Sahin 2022-03-15 16:16:17 +03:00
commit b4df6e1ae8
33 changed files with 177 additions and 142 deletions

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

@ -0,0 +1 @@
Support both stable and unstable prefixes for Events about Polls and Location

View file

@ -68,7 +68,7 @@ class PollAggregationTest : InstrumentedTest {
val aliceEventsListener = object : Timeline.Listener {
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
snapshot.firstOrNull { it.root.getClearType() == EventType.POLL_START }?.let { pollEvent ->
snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START }?.let { pollEvent ->
val pollEventId = pollEvent.eventId
val pollContent = pollEvent.root.content?.toModel<MessagePollContent>()
val pollSummary = pollEvent.annotations?.pollResponseSummary
@ -83,25 +83,25 @@ class PollAggregationTest : InstrumentedTest {
// Poll has just been created.
testInitialPollConditions(pollContent, pollSummary)
lock.countDown()
roomFromBobPOV.voteToPoll(pollEventId, pollContent.pollCreationInfo?.answers?.firstOrNull()?.id ?: "")
roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
}
TOTAL_TEST_COUNT - 1 -> {
// Bob: Option 1
testBobVotesOption1(pollContent, pollSummary)
lock.countDown()
roomFromBobPOV.voteToPoll(pollEventId, pollContent.pollCreationInfo?.answers?.get(1)?.id ?: "")
roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
}
TOTAL_TEST_COUNT - 2 -> {
// Bob: Option 2
testBobChangesVoteToOption2(pollContent, pollSummary)
lock.countDown()
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.pollCreationInfo?.answers?.get(1)?.id ?: "")
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
}
TOTAL_TEST_COUNT - 3 -> {
// Alice: Option 2, Bob: Option 2
testAliceAndBobVoteToOption2(pollContent, pollSummary)
lock.countDown()
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.pollCreationInfo?.answers?.firstOrNull()?.id ?: "")
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
}
TOTAL_TEST_COUNT - 4 -> {
// Alice: Option 1, Bob: Option 2
@ -113,7 +113,7 @@ class PollAggregationTest : InstrumentedTest {
// Alice: Option 1, Bob: Option 2 [poll is ended]
testEndedPoll(pollSummary)
lock.countDown()
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.pollCreationInfo?.answers?.get(1)?.id ?: "")
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
}
TOTAL_TEST_COUNT - 6 -> {
// Alice: Option 1 (ignore change), Bob: Option 2 [poll is ended]
@ -144,11 +144,11 @@ class PollAggregationTest : InstrumentedTest {
// No votes yet, poll summary should be null
pollSummary shouldBe null
// Question should be the same as intended
pollContent.pollCreationInfo?.question?.question shouldBeEqualTo pollQuestion
pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() shouldBeEqualTo pollQuestion
// Options should be the same as intended
pollContent.pollCreationInfo?.answers?.let { answers ->
pollContent.getBestPollCreationInfo()?.answers?.let { answers ->
answers.size shouldBeEqualTo pollOptions.size
answers.map { it.answer } shouldContainAll pollOptions
answers.map { it.getBestAnswer() } shouldContainAll pollOptions
}
}
@ -157,7 +157,7 @@ class PollAggregationTest : InstrumentedTest {
fail("Poll summary shouldn't be null when someone votes")
return
}
val answerId = pollContent.pollCreationInfo?.answers?.first()?.id
val answerId = pollContent.getBestPollCreationInfo()?.answers?.first()?.id
// Check if the intended vote is in poll summary
pollSummary.aggregatedContent?.let { aggregatedContent ->
assertTotalVotesCount(aggregatedContent, 1)
@ -172,7 +172,7 @@ class PollAggregationTest : InstrumentedTest {
fail("Poll summary shouldn't be null when someone votes")
return
}
val answerId = pollContent.pollCreationInfo?.answers?.get(1)?.id
val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
// Check if the intended vote is in poll summary
pollSummary.aggregatedContent?.let { aggregatedContent ->
assertTotalVotesCount(aggregatedContent, 1)
@ -187,7 +187,7 @@ class PollAggregationTest : InstrumentedTest {
fail("Poll summary shouldn't be null when someone votes")
return
}
val answerId = pollContent.pollCreationInfo?.answers?.get(1)?.id
val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
// Check if the intended votes is in poll summary
pollSummary.aggregatedContent?.let { aggregatedContent ->
assertTotalVotesCount(aggregatedContent, 2)
@ -203,8 +203,8 @@ class PollAggregationTest : InstrumentedTest {
fail("Poll summary shouldn't be null when someone votes")
return
}
val firstAnswerId = pollContent.pollCreationInfo?.answers?.firstOrNull()?.id
val secondAnswerId = pollContent.pollCreationInfo?.answers?.get(1)?.id
val firstAnswerId = pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id
val secondAnswerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
// Check if the intended votes is in poll summary
pollSummary.aggregatedContent?.let { aggregatedContent ->
assertTotalVotesCount(aggregatedContent, 2)

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
@ -372,7 +372,7 @@ fun Event.getRelationContent(): RelationDefaultContent? {
* Returns the poll question or null otherwise
*/
fun Event.getPollQuestion(): String? =
getPollContent()?.pollCreationInfo?.question?.question
getPollContent()?.getBestPollCreationInfo()?.question?.getBestQuestion()
/**
* Returns the relation content for a specific type or null otherwise

View file

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

View file

@ -39,37 +39,46 @@ data class MessageLocationContent(
*/
@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
*/
@Json(name = "org.matrix.msc3488.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,
@Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
@Json(name = "m.location") val locationInfo: LocationInfo? = 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
* inventories, geofencing, checkins/checkouts etc.
* 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.
*/
@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
@Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
@Json(name = "m.asset") val locationAsset: LocationAsset? = null
) : 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.
*/
fun isSelfLocation(): Boolean {
// Should behave like m.self if locationAsset is null
val locationAsset = getBestLocationAsset()
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 = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
@Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
) : MessageContent
@Json(name = "org.matrix.msc3381.poll.start") val unstablePollCreationInfo: PollCreationInfo? = null,
@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 = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
@Json(name = "org.matrix.msc3381.poll.response") val response: PollResponse? = null
) : MessageContent
@Json(name = "org.matrix.msc3381.poll.response") val unstableResponse: PollResponse? = null,
@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)
data class PollAnswer(
@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

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

View file

@ -21,5 +21,9 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
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.
*/
@Json(name = "org.matrix.msc3381.poll.disclosed")
DISCLOSED_UNSTABLE,
@Json(name = "m.poll.disclosed")
DISCLOSED,
/**
* Results should be only revealed when the poll is ended.
*/
@Json(name = "org.matrix.msc3381.poll.undisclosed")
UNDISCLOSED_UNSTABLE,
@Json(name = "m.poll.undisclosed")
UNDISCLOSED
}

View file

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

View file

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

View file

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

View file

@ -86,11 +86,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
// TODO Add ?
// EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY,
EventType.ENCRYPTED,
EventType.POLL_START,
EventType.POLL_RESPONSE,
EventType.POLL_END
)
EventType.ENCRYPTED
) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
return allowedTypes.contains(eventType)
@ -156,7 +153,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace!
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 ->
Timber.v("###RESPONSE in room $roomId for event ${event.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)
}
}
EventType.POLL_RESPONSE -> {
in EventType.POLL_RESPONSE -> {
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId)
}
}
EventType.POLL_END -> {
in EventType.POLL_END -> {
event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
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()
if (content?.relatesTo?.type == RelationType.REPLACE) {
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)
}
}
EventType.POLL_RESPONSE -> {
in EventType.POLL_RESPONSE -> {
event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
handleResponse(realm, event, it, roomId, isLocalEcho)
}
}
EventType.POLL_END -> {
in EventType.POLL_END -> {
event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
handleEndPoll(realm, event, it, roomId, isLocalEcho)
}
@ -407,12 +404,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
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}")
}
// 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")
return
}

View file

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

View file

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

View file

@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
fun TimelineEvent.canReact(): Boolean {
// 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.isRedacted()
}

View file

@ -1189,7 +1189,7 @@ class TimelineFragment @Inject constructor(
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
getString(R.string.voice_message_reply_content, formattedDuration)
} else if (messageContent is MessagePollContent) {
messageContent.pollCreationInfo?.question?.question
messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
} else {
messageContent?.body ?: ""
}
@ -2165,7 +2165,7 @@ class TimelineFragment @Inject constructor(
timelineViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
}
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)
} else if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))

View file

@ -181,7 +181,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} else {
when (timelineEvent.root.getClearType()) {
EventType.MESSAGE,
EventType.STICKER -> {
EventType.STICKER -> {
val messageContent: MessageContent? = timelineEvent.getLastMessageContent()
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
val html = messageContent.formattedBody
@ -207,13 +207,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
EventType.CALL_INVITE,
EventType.CALL_CANDIDATES,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> {
EventType.CALL_ANSWER -> {
noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
}
EventType.POLL_START -> {
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.pollCreationInfo?.question?.question ?: ""
in EventType.POLL_START -> {
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)
?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
}
else -> null
else -> null
}
}
} catch (failure: Throwable) {
@ -373,7 +374,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}
if (canRedact(timelineEvent, actionPermissions)) {
if (timelineEvent.root.getClearType() == EventType.POLL_START) {
if (timelineEvent.root.getClearType() in EventType.POLL_START) {
add(EventSharedAction.Redact(
eventId,
askForReason = informationData.senderId != session.myUserId,
@ -425,7 +426,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
// 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
return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT,
@ -511,7 +512,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
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
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
if (event.root.senderId == session.myUserId) return true
// Check permission for messages sent by other users
@ -526,13 +527,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun canViewReactions(event: TimelineEvent): Boolean {
// 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
}
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
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
// TODO if user is admin or moderator
val messageContent = event.root.getClearContent().toModel<MessageContent>()
@ -578,13 +579,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}
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) &&
event.annotations?.pollResponseSummary?.closedTime == null
}
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?.aggregatedContent?.totalVotes ?: 0 == 0
}

View file

@ -247,7 +247,7 @@ class MessageItemFactory @Inject constructor(
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
val winnerVoteCount = pollResponseSummary?.winnerVoteCount
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 {
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 isMyVote = pollResponseSummary?.myVote == option.id
val voteCount = voteSummary?.total ?: 0
val votePercentage = voteSummary?.percentage ?: 0.0
val optionId = option.id ?: ""
val optionAnswer = option.answer ?: ""
val optionAnswer = option.getBestAnswer() ?: ""
optionViewStates.add(
if (!isPollSent) {
@ -291,7 +291,7 @@ class MessageItemFactory @Inject constructor(
)
}
val question = pollContent.pollCreationInfo?.question?.question ?: ""
val question = pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
return PollItem_()
.attributes(attributes)

View file

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

View file

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

View file

@ -106,8 +106,8 @@ class NoticeEventFormatter @Inject constructor(
EventType.STATE_SPACE_PARENT,
EventType.REDACTION,
EventType.STICKER,
EventType.POLL_RESPONSE,
EventType.POLL_END -> formatDebug(timelineEvent.root)
in EventType.POLL_RESPONSE,
in EventType.POLL_END -> formatDebug(timelineEvent.root)
else -> {
Timber.v("Type $type not handled by this formatter")
null
@ -196,8 +196,8 @@ class NoticeEventFormatter @Inject constructor(
}
private fun formatDebug(event: Event): CharSequence {
val threadPrefix = if (event.isThread()) "thread" else ""
return "Debug: $threadPrefix event type \"${event.getClearType()}\""
val threadPrefix = if (event.isThread()) "thread" else ""
return "Debug: $threadPrefix event type \"${event.getClearType()}\""
}
private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? {

View file

@ -50,9 +50,8 @@ object TimelineDisplayableEvents {
EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_JOIN_RULES,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL,
EventType.POLL_START
)
EventType.KEY_VERIFICATION_CANCEL
) + EventType.POLL_START
}
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
private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf(
EventType.MESSAGE,
EventType.POLL_START,
EventType.ENCRYPTED,
EventType.STICKER
)
) + EventType.POLL_START
// Can't be rendered in bubbles, so get back to default layout
private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf(

View file

@ -39,7 +39,7 @@ class UrlMapProvider @Inject constructor(
suspend fun getMapUrl(): String {
val upstreamMapUrl = tryOrNull { rawService.getElementWellknown(session.sessionParams) }
?.mapTileServerConfig
?.getBestMapTileServerConfig()
?.mapStyleUrl
return upstreamMapUrl ?: fallbackMapUrl
}

View file

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

View file

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

View file

@ -27,7 +27,7 @@ data class CreatePollViewState(
val options: List<String> = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" },
val canCreatePoll: Boolean = false,
val canAddMoreOptions: Boolean = true,
val pollType: PollType = PollType.DISCLOSED
val pollType: PollType = PollType.DISCLOSED_UNSTABLE
) : MavericksState {
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>() {
@EpoxyAttribute
var pollType: PollType = PollType.DISCLOSED
var pollType: PollType = PollType.DISCLOSED_UNSTABLE
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var pollTypeChangedListener: RadioGroup.OnCheckedChangeListener? = null
@ -38,8 +38,8 @@ abstract class PollTypeSelectionItem : VectorEpoxyModel<PollTypeSelectionItem.Ho
holder.pollTypeRadioGroup.check(
when (pollType) {
PollType.DISCLOSED -> R.id.openPollTypeRadioButton
PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton
PollType.DISCLOSED_UNSTABLE, PollType.DISCLOSED -> R.id.openPollTypeRadioButton
PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton
}
)

View file

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

View file

@ -22,6 +22,7 @@ import org.amshove.kluent.shouldBeTrue
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.LocationAssetType
import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
class LocationDataTest {
@ -64,13 +65,24 @@ class LocationDataTest {
@Test
fun selfLocationTest() {
val contentWithNullAsset = MessageLocationContent(body = "", geoUri = "", locationAsset = null)
val contentWithNullAsset = MessageLocationContent(body = "", geoUri = "")
contentWithNullAsset.isSelfLocation().shouldBeTrue()
val contentWithNullAssetType = MessageLocationContent(body = "", geoUri = "", locationAsset = LocationAsset(type = null))
val contentWithNullAssetType = MessageLocationContent(body = "", geoUri = "", unstableLocationAsset = LocationAsset(type = null))
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()
}
@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)
}
}