From e4ce4ab1ab0633ea4b33668ca20d6dc2aa8210ab Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Wed, 9 Mar 2022 12:07:16 +0100 Subject: [PATCH 01/27] Adds server side vote blocking for ended polls --- .../room/detail/timeline/item/PollItem.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 2327a0f2e2..0ce225b21c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -44,8 +44,8 @@ abstract class PollItem : AbsMessageItem() { @EpoxyAttribute var totalVotesText: String? = null - @EpoxyAttribute - var edited: Boolean = false + @EpoxyAttribute + var edited: Boolean = false @EpoxyAttribute lateinit var optionViewStates: List @@ -54,7 +54,6 @@ abstract class PollItem : AbsMessageItem() { override fun bind(holder: Holder) { super.bind(holder) - val relatedEventId = eventId ?: return renderSendState(holder.view, holder.questionTextView) @@ -73,13 +72,21 @@ abstract class PollItem : AbsMessageItem() { optionViewStates.forEachIndexed { index, optionViewState -> views.getOrNull(index)?.let { it.render(optionViewState) - it.setOnClickListener { - callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId)) - } + it.setOnClickListener { onPollItemClick(optionViewState) } } } } + private fun onPollItemClick(optionViewState: PollOptionViewState) { + val relatedEventId = eventId + + if (isPollActive(optionViewState) && relatedEventId != null) { + callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId)) + } + } + + private fun isPollActive(optionViewState: PollOptionViewState) = optionViewState !is PollOptionViewState.PollEnded + class Holder : AbsMessageItem.Holder(STUB_ID) { val questionTextView by bind(R.id.questionTextView) val optionsContainer by bind(R.id.optionsContainer) From 080844dc9de121c80b15034ff77dd4109913b7f3 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Thu, 10 Mar 2022 16:28:22 +0100 Subject: [PATCH 02/27] Removes event timestamp condition for sdk poll voting --- .../session/room/EventRelationsAggregationProcessor.kt | 2 +- .../app/features/home/room/detail/timeline/item/PollItem.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 1e0eb8b497..d186f74a94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -385,7 +385,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } val closedTime = existingPollSummary?.closedTime - if (closedTime != null && eventTimestamp > closedTime) { + if (closedTime != null) { Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}") return } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 0ce225b21c..295f4c930b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -85,7 +85,8 @@ abstract class PollItem : AbsMessageItem() { } } - private fun isPollActive(optionViewState: PollOptionViewState) = optionViewState !is PollOptionViewState.PollEnded + private fun isPollActive(optionViewState: PollOptionViewState) = true // TODO: Revert (true for debugging) +// private fun isPollActive(optionViewState: PollOptionViewState) = optionViewState !is PollOptionViewState.PollEnded class Holder : AbsMessageItem.Holder(STUB_ID) { val questionTextView by bind(R.id.questionTextView) From 628a160c3a2bf9f1c785474d0c5624795f785265 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Thu, 10 Mar 2022 16:32:42 +0100 Subject: [PATCH 03/27] Reverts timestamp condition but changes timing of setting closedTime --- .../session/room/EventRelationsAggregationProcessor.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index d186f74a94..f3f55466da 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -385,7 +385,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } val closedTime = existingPollSummary?.closedTime - if (closedTime != null) { + if (closedTime != null && eventTimestamp > closedTime) { Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}") return } @@ -499,6 +499,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } val txId = event.unsignedData?.transactionId + existingPollSummary.closedTime = event.originServerTs + // is it a remote echo? if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { // ok it has already been managed @@ -507,8 +509,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( existingPollSummary.sourceEvents.add(event.eventId) return } - - existingPollSummary.closedTime = event.originServerTs } private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? { From df6ae4b8482975f185458d9af8c264f35c1ae925 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Thu, 10 Mar 2022 18:26:49 +0100 Subject: [PATCH 04/27] Fixes warning for debugging --- .../app/features/home/room/detail/timeline/item/PollItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 295f4c930b..5727c8583d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -85,7 +85,7 @@ abstract class PollItem : AbsMessageItem() { } } - private fun isPollActive(optionViewState: PollOptionViewState) = true // TODO: Revert (true for debugging) + private fun isPollActive(optionViewState: PollOptionViewState) = optionViewState.let { true } // TODO: Revert (true for debugging) // private fun isPollActive(optionViewState: PollOptionViewState) = optionViewState !is PollOptionViewState.PollEnded class Holder : AbsMessageItem.Holder(STUB_ID) { From fe3c9cc09f07c642410e0a76f2aeef53c35d8041 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Thu, 10 Mar 2022 19:15:12 +0100 Subject: [PATCH 05/27] Reverts to fix by removing event timestamp condition --- .../session/room/EventRelationsAggregationProcessor.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index f3f55466da..d186f74a94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -385,7 +385,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } val closedTime = existingPollSummary?.closedTime - if (closedTime != null && eventTimestamp > closedTime) { + if (closedTime != null) { Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}") return } @@ -499,8 +499,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } val txId = event.unsignedData?.transactionId - existingPollSummary.closedTime = event.originServerTs - // is it a remote echo? if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { // ok it has already been managed @@ -509,6 +507,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( existingPollSummary.sourceEvents.add(event.eventId) return } + + existingPollSummary.closedTime = event.originServerTs } private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? { From 6db1c377c42e81193d332cd33b3a5506a70b306a Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Thu, 10 Mar 2022 19:16:22 +0100 Subject: [PATCH 06/27] Reverts to fix by removing event timestamp condition --- changelog.d/5473.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5473.bugfix diff --git a/changelog.d/5473.bugfix b/changelog.d/5473.bugfix new file mode 100644 index 0000000000..e53329e202 --- /dev/null +++ b/changelog.d/5473.bugfix @@ -0,0 +1 @@ +Fixes polls being votable after being ended From 6a59007eb5c916047673103e57675c0c66d43c64 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Thu, 10 Mar 2022 21:26:08 +0100 Subject: [PATCH 07/27] Reverts debug isPollActive --- .../app/features/home/room/detail/timeline/item/PollItem.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 5727c8583d..0ce225b21c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -85,8 +85,7 @@ abstract class PollItem : AbsMessageItem() { } } - private fun isPollActive(optionViewState: PollOptionViewState) = optionViewState.let { true } // TODO: Revert (true for debugging) -// private fun isPollActive(optionViewState: PollOptionViewState) = optionViewState !is PollOptionViewState.PollEnded + private fun isPollActive(optionViewState: PollOptionViewState) = optionViewState !is PollOptionViewState.PollEnded class Holder : AbsMessageItem.Holder(STUB_ID) { val questionTextView by bind(R.id.questionTextView) From 610c67c208f5134f0cecd3bc4ad1781504e75cb1 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Sun, 13 Mar 2022 20:09:14 +0100 Subject: [PATCH 08/27] Refactors buildPollItem in MessageItemFactory --- .../home/room/detail/TimelineFragment.kt | 18 +- .../timeline/factory/MessageItemFactory.kt | 775 ++++++++++-------- .../room/detail/timeline/item/PollItem.kt | 6 +- .../features/navigation/DefaultNavigator.kt | 2 +- .../app/features/navigation/Navigator.kt | 2 +- .../features/poll/{create => }/PollMode.kt | 2 +- .../im/vector/app/features/poll/PollState.kt | 27 + .../poll/create/CreatePollFragment.kt | 1 + .../poll/create/CreatePollViewModel.kt | 1 + .../poll/create/CreatePollViewState.kt | 26 +- 10 files changed, 467 insertions(+), 393 deletions(-) rename vector/src/main/java/im/vector/app/features/poll/{create => }/PollMode.kt (93%) create mode 100644 vector/src/main/java/im/vector/app/features/poll/PollState.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 662af3d546..9afaf89718 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -182,7 +182,7 @@ import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.PermalinkHandler -import im.vector.app.features.poll.create.PollMode +import im.vector.app.features.poll.PollMode import im.vector.app.features.reactions.EmojiReactionPickerActivity import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.session.coroutineScope @@ -2165,12 +2165,16 @@ class TimelineFragment @Inject constructor( timelineViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } is EventSharedAction.Edit -> { - if (action.eventType == 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())) - } else { - requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) + when { + action.eventType == EventType.POLL_START -> { + navigator.openCreatePoll(requireContext(), timelineArgs.roomId, action.eventId, PollMode.EDIT) + } + withState(messageComposerViewModel) { it.isVoiceMessageIdle } -> { + messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) + } + else -> { + requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) + } } } is EventSharedAction.Quote -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index aa1758dd6c..2cc87dfef5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -56,7 +56,12 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_ import im.vector.app.features.home.room.detail.timeline.item.PollItem import im.vector.app.features.home.room.detail.timeline.item.PollItem_ -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollEnded +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollReady +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollSending +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollUndisclosed +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollVoted +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem @@ -73,6 +78,12 @@ import im.vector.app.features.location.UrlMapProvider import im.vector.app.features.location.toLocationData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer +import im.vector.app.features.poll.PollState +import im.vector.app.features.poll.PollState.Ended +import im.vector.app.features.poll.PollState.Ready +import im.vector.app.features.poll.PollState.Sending +import im.vector.app.features.poll.PollState.Undisclosed +import im.vector.app.features.poll.PollState.Voted import im.vector.app.features.settings.VectorPreferences import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span @@ -95,6 +106,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.api.session.room.model.message.PollAnswer import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileUrl @@ -107,30 +119,30 @@ import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsS import javax.inject.Inject class MessageItemFactory @Inject constructor( - private val localFilesHelper: LocalFilesHelper, - private val colorProvider: ColorProvider, - private val dimensionConverter: DimensionConverter, - private val timelineMediaSizeProvider: TimelineMediaSizeProvider, - private val htmlRenderer: Lazy, - private val htmlCompressor: VectorHtmlCompressor, - private val textRendererFactory: EventTextRenderer.Factory, - private val stringProvider: StringProvider, - private val imageContentRenderer: ImageContentRenderer, - private val messageInformationDataFactory: MessageInformationDataFactory, - private val messageItemAttributesFactory: MessageItemAttributesFactory, - private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, - private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, - private val defaultItemFactory: DefaultItemFactory, - private val noticeItemFactory: NoticeItemFactory, - private val avatarSizeProvider: AvatarSizeProvider, - private val pillsPostProcessorFactory: PillsPostProcessor.Factory, - private val lightweightSettingsStorage: LightweightSettingsStorage, - private val spanUtils: SpanUtils, - private val session: Session, - private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker, - private val locationPinProvider: LocationPinProvider, - private val vectorPreferences: VectorPreferences, - private val urlMapProvider: UrlMapProvider, + private val localFilesHelper: LocalFilesHelper, + private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, + private val timelineMediaSizeProvider: TimelineMediaSizeProvider, + private val htmlRenderer: Lazy, + private val htmlCompressor: VectorHtmlCompressor, + private val textRendererFactory: EventTextRenderer.Factory, + private val stringProvider: StringProvider, + private val imageContentRenderer: ImageContentRenderer, + private val messageInformationDataFactory: MessageInformationDataFactory, + private val messageItemAttributesFactory: MessageItemAttributesFactory, + private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, + private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, + private val defaultItemFactory: DefaultItemFactory, + private val noticeItemFactory: NoticeItemFactory, + private val avatarSizeProvider: AvatarSizeProvider, + private val pillsPostProcessorFactory: PillsPostProcessor.Factory, + private val lightweightSettingsStorage: LightweightSettingsStorage, + private val spanUtils: SpanUtils, + private val session: Session, + private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker, + private val locationPinProvider: LocationPinProvider, + private val vectorPreferences: VectorPreferences, + private val urlMapProvider: UrlMapProvider, ) { // TODO inject this properly? @@ -165,7 +177,7 @@ class MessageItemFactory @Inject constructor( return defaultItemFactory.create(malformedText, informationData, highlight, callback) } if (messageContent.relatesTo?.type == RelationType.REPLACE || - event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE + event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should display it when debugging as a notice event return noticeItemFactory.create(params) @@ -179,16 +191,16 @@ class MessageItemFactory @Inject constructor( // always hide summary when we are on thread timeline val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, threadDetails) -// val all = event.root.toContent() -// val ev = all.toModel() + // val all = event.root.toContent() + // val ev = all.toModel() val messageItem = when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) - is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) - is MessageAudioContent -> { + is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) + is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) + is MessageAudioContent -> { if (messageContent.voiceMessageIndicator != null) { buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes) } else { @@ -196,25 +208,27 @@ class MessageItemFactory @Inject constructor( } } is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) - is MessageLocationContent -> { + is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) + is MessageLocationContent -> { if (vectorPreferences.labsRenderLocationsInTimeline()) { buildLocationItem(messageContent, informationData, highlight, attributes) } else { buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } } - else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) + else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } return messageItem?.apply { layout(informationData.messageLayout.layoutRes) } } - private fun buildLocationItem(locationContent: MessageLocationContent, - informationData: MessageInformationData, - highlight: Boolean, - attributes: AbsMessageItem.Attributes): MessageLocationItem? { + private fun buildLocationItem( + locationContent: MessageLocationContent, + informationData: MessageInformationData, + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + ): MessageLocationItem? { val width = timelineMediaSizeProvider.getMaxSize().first val height = dimensionConverter.dpToPx(200) @@ -225,98 +239,109 @@ class MessageItemFactory @Inject constructor( val userId = if (locationContent.isSelfLocation()) informationData.senderId else null return MessageLocationItem_() - .attributes(attributes) - .locationUrl(locationUrl) - .mapWidth(width) - .mapHeight(height) - .userId(userId) - .locationPinProvider(locationPinProvider) - .highlighted(highlight) - .leftGuideline(avatarSizeProvider.leftGuideline) + .attributes(attributes) + .locationUrl(locationUrl) + .mapWidth(width) + .mapHeight(height) + .userId(userId) + .locationPinProvider(locationPinProvider) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) } - private fun buildPollItem(pollContent: MessagePollContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): PollItem? { - val optionViewStates = mutableListOf() - + private fun buildPollItem( + pollContent: MessagePollContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): PollItem { val pollResponseSummary = informationData.pollResponseAggregatedSummary - val isEnded = pollResponseSummary?.isClosed.orFalse() - val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() - val winnerVoteCount = pollResponseSummary?.winnerVoteCount - val isPollSent = informationData.sendState.isSent() - val isPollUndisclosed = pollContent.pollCreationInfo?.kind == PollType.UNDISCLOSED - - val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let { - when { - isEnded -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it) - isPollUndisclosed -> "" - didUserVoted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it) - else -> if (it == 0) { - stringProvider.getString(R.string.poll_no_votes_cast) - } else { - stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, it, it) - } - } - } - - pollContent.pollCreationInfo?.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 ?: "" - - optionViewStates.add( - if (!isPollSent) { - // Poll event is not send yet. Disable option. - PollOptionViewState.PollSending(optionId, optionAnswer) - } else if (isEnded) { - // Poll is ended. Disable option, show votes and mark the winner. - val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount - PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) - } else if (isPollUndisclosed) { - // Poll is closed. Enable option, hide votes and mark the user's selection. - PollOptionViewState.PollUndisclosed(optionId, optionAnswer, isMyVote) - } else if (didUserVoted) { - // User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection. - PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) - } else { - // User didn't voted yet and poll is not ended yet. Enable options, hide votes. - PollOptionViewState.PollReady(optionId, optionAnswer) - } - ) - } - - val question = pollContent.pollCreationInfo?.question?.question ?: "" + val pollState = createPollState(informationData, pollResponseSummary, pollContent) + val optionViewStates = pollContent.pollCreationInfo?.answers?.mapToOptions(pollState, informationData) + val questionText = pollContent.pollCreationInfo?.question?.question.orEmpty() + val question = createPollQuestion(informationData, questionText, callback) + val totalVotesText = createTotalVotesText(pollState, pollResponseSummary) return PollItem_() - .attributes(attributes) - .eventId(informationData.eventId) - .pollQuestion( - if (informationData.hasBeenEdited) { - annotateWithEdited(question, callback, informationData) - } else { - question - }.toEpoxyCharSequence() - ) - .pollSent(isPollSent) - .totalVotesText(totalVotesText) - .optionViewStates(optionViewStates) - .edited(informationData.hasBeenEdited) - .highlighted(highlight) - .leftGuideline(avatarSizeProvider.leftGuideline) - .callback(callback) + .attributes(attributes) + .eventId(informationData.eventId) + .pollQuestion(question) + .canVote(pollState.isVotable()) + .totalVotesText(totalVotesText) + .optionViewStates(optionViewStates) + .edited(informationData.hasBeenEdited) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .callback(callback) } - private fun buildAudioMessageItem(messageContent: MessageAudioContent, - @Suppress("UNUSED_PARAMETER") - informationData: MessageInformationData, - highlight: Boolean, - attributes: AbsMessageItem.Attributes): MessageFileItem? { + private fun createPollState( + informationData: MessageInformationData, + pollResponseSummary: PollResponseData?, + pollContent: MessagePollContent, + ): PollState = when { + !informationData.sendState.isSent() -> Sending + pollResponseSummary?.isClosed.orFalse() -> Ended + pollContent.pollCreationInfo?.kind == PollType.UNDISCLOSED -> Undisclosed + pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> Voted(pollResponseSummary?.totalVotes ?: 0) + else -> Ready + } + + private fun List.mapToOptions( + pollState: PollState, + informationData: MessageInformationData, + ) = map { answer -> + val pollResponseSummary = informationData.pollResponseAggregatedSummary + val winnerVoteCount = pollResponseSummary?.winnerVoteCount + val optionId = answer.id ?: "" + val optionAnswer = answer.answer ?: "" + val voteSummary = pollResponseSummary?.votes?.get(answer.id) + val voteCount = voteSummary?.total ?: 0 + val votePercentage = voteSummary?.percentage ?: 0.0 + val isMyVote = pollResponseSummary?.myVote == answer.id + val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount + + when (pollState) { + Sending -> PollSending(optionId, optionAnswer) + Ready -> PollReady(optionId, optionAnswer) + is Voted -> PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) + Undisclosed -> PollUndisclosed(optionId, optionAnswer, isMyVote) + Ended -> PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) + } + } + + private fun createPollQuestion( + informationData: MessageInformationData, + question: String, + callback: TimelineEventController.Callback?, + ) = if (informationData.hasBeenEdited) { + annotateWithEdited(question, callback, informationData) + } else { + question + }.toEpoxyCharSequence() + + private fun createTotalVotesText( + pollState: PollState, + pollResponseSummary: PollResponseData?, + ): String { + val votes = pollResponseSummary?.totalVotes ?: 0 + return when { + pollState is Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes) + pollState is Undisclosed -> "" + pollState is Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes) + votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast) + else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes) + } + } + + private fun buildAudioMessageItem( + messageContent: MessageAudioContent, + @Suppress("UNUSED_PARAMETER") + informationData: MessageInformationData, + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + ): MessageFileItem? { val fileUrl = messageContent.getFileUrl()?.let { if (informationData.sentByMe && !informationData.sendState.isSent()) { it @@ -325,29 +350,31 @@ class MessageItemFactory @Inject constructor( } } ?: "" return MessageFileItem_() - .attributes(attributes) - .izLocalFile(localFilesHelper.isLocalFile(fileUrl)) - .izDownloaded(session.fileService().isFileInCache( - fileUrl, - messageContent.getFileName(), - messageContent.mimeType, - messageContent.encryptedFileInfo?.toElementToDecrypt()) - ) - .mxcUrl(fileUrl) - .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) - .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) - .highlighted(highlight) - .leftGuideline(avatarSizeProvider.leftGuideline) - .filename(messageContent.body) - .iconRes(R.drawable.ic_headphones) + .attributes(attributes) + .izLocalFile(localFilesHelper.isLocalFile(fileUrl)) + .izDownloaded(session.fileService().isFileInCache( + fileUrl, + messageContent.getFileName(), + messageContent.mimeType, + messageContent.encryptedFileInfo?.toElementToDecrypt()) + ) + .mxcUrl(fileUrl) + .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) + .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .filename(messageContent.body) + .iconRes(R.drawable.ic_headphones) } - private fun buildVoiceMessageItem(params: TimelineItemFactoryParams, - messageContent: MessageAudioContent, - @Suppress("UNUSED_PARAMETER") - informationData: MessageInformationData, - highlight: Boolean, - attributes: AbsMessageItem.Attributes): MessageVoiceItem? { + private fun buildVoiceMessageItem( + params: TimelineItemFactoryParams, + messageContent: MessageAudioContent, + @Suppress("UNUSED_PARAMETER") + informationData: MessageInformationData, + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + ): MessageVoiceItem? { val fileUrl = messageContent.getFileUrl()?.let { if (informationData.sentByMe && !informationData.sendState.isSent()) { it @@ -363,31 +390,33 @@ class MessageItemFactory @Inject constructor( } return MessageVoiceItem_() - .attributes(attributes) - .duration(messageContent.audioWaveformInfo?.duration ?: 0) - .waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty()) - .playbackControlButtonClickListener(playbackControlButtonClickListener) - .voiceMessagePlaybackTracker(voiceMessagePlaybackTracker) - .izLocalFile(localFilesHelper.isLocalFile(fileUrl)) - .izDownloaded(session.fileService().isFileInCache( - fileUrl, - messageContent.getFileName(), - messageContent.mimeType, - messageContent.encryptedFileInfo?.toElementToDecrypt()) - ) - .mxcUrl(fileUrl) - .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) - .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) - .highlighted(highlight) - .leftGuideline(avatarSizeProvider.leftGuideline) + .attributes(attributes) + .duration(messageContent.audioWaveformInfo?.duration ?: 0) + .waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty()) + .playbackControlButtonClickListener(playbackControlButtonClickListener) + .voiceMessagePlaybackTracker(voiceMessagePlaybackTracker) + .izLocalFile(localFilesHelper.isLocalFile(fileUrl)) + .izDownloaded(session.fileService().isFileInCache( + fileUrl, + messageContent.getFileName(), + messageContent.mimeType, + messageContent.encryptedFileInfo?.toElementToDecrypt()) + ) + .mxcUrl(fileUrl) + .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) + .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) } - private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent, - @Suppress("UNUSED_PARAMETER") - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): VerificationRequestItem? { + private fun buildVerificationRequestMessageItem( + messageContent: MessageVerificationRequestContent, + @Suppress("UNUSED_PARAMETER") + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): VerificationRequestItem? { // If this request is not sent by me or sent to me, we should ignore it in timeline val myUserId = session.myUserId if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) { @@ -401,136 +430,144 @@ class MessageItemFactory @Inject constructor( informationData.memberName } return VerificationRequestItem_() - .attributes( - VerificationRequestItem.Attributes( - otherUserId = otherUserId, - otherUserName = otherUserName.toString(), - referenceId = informationData.eventId, - informationData = informationData, - avatarRenderer = attributes.avatarRenderer, - messageColorProvider = attributes.messageColorProvider, - itemLongClickListener = attributes.itemLongClickListener, - itemClickListener = attributes.itemClickListener, - reactionPillCallback = attributes.reactionPillCallback, - readReceiptsCallback = attributes.readReceiptsCallback, - emojiTypeFace = attributes.emojiTypeFace - ) + .attributes( + VerificationRequestItem.Attributes( + otherUserId = otherUserId, + otherUserName = otherUserName.toString(), + referenceId = informationData.eventId, + informationData = informationData, + avatarRenderer = attributes.avatarRenderer, + messageColorProvider = attributes.messageColorProvider, + itemLongClickListener = attributes.itemLongClickListener, + itemClickListener = attributes.itemClickListener, + reactionPillCallback = attributes.reactionPillCallback, + readReceiptsCallback = attributes.readReceiptsCallback, + emojiTypeFace = attributes.emojiTypeFace ) - .callback(callback) - .highlighted(highlight) - .leftGuideline(avatarSizeProvider.leftGuideline) + ) + .callback(callback) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) } - private fun buildFileMessageItem(messageContent: MessageFileContent, -// informationData: MessageInformationData, - highlight: Boolean, -// callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): MessageFileItem? { + private fun buildFileMessageItem( + messageContent: MessageFileContent, + highlight: Boolean, + attributes: AbsMessageItem.Attributes, + ): MessageFileItem? { val mxcUrl = messageContent.getFileUrl() ?: "" return MessageFileItem_() - .attributes(attributes) - .leftGuideline(avatarSizeProvider.leftGuideline) - .izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl())) - .izDownloaded(session.fileService().isFileInCache(messageContent)) - .mxcUrl(mxcUrl) - .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) - .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) - .highlighted(highlight) - .filename(messageContent.body) - .iconRes(R.drawable.ic_paperclip) + .attributes(attributes) + .leftGuideline(avatarSizeProvider.leftGuideline) + .izLocalFile(localFilesHelper.isLocalFile(messageContent.getFileUrl())) + .izDownloaded(session.fileService().isFileInCache(messageContent)) + .mxcUrl(mxcUrl) + .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) + .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) + .highlighted(highlight) + .filename(messageContent.body) + .iconRes(R.drawable.ic_paperclip) } - private fun buildNotHandledMessageItem(messageContent: MessageContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): MessageTextItem? { + private fun buildNotHandledMessageItem( + messageContent: MessageContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): MessageTextItem? { // For compatibility reason we should display the body return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } - private fun buildImageMessageItem(messageContent: MessageImageInfoContent, - @Suppress("UNUSED_PARAMETER") - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): MessageImageVideoItem? { + private fun buildImageMessageItem( + messageContent: MessageImageInfoContent, + @Suppress("UNUSED_PARAMETER") + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): MessageImageVideoItem? { val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val data = ImageContentRenderer.Data( - eventId = informationData.eventId, - filename = messageContent.body, - mimeType = messageContent.mimeType, - url = messageContent.getFileUrl(), - elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), - height = messageContent.info?.height, - maxHeight = maxHeight, - width = messageContent.info?.width, - maxWidth = maxWidth, - allowNonMxcUrls = informationData.sendState.isSending() + eventId = informationData.eventId, + filename = messageContent.body, + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), + height = messageContent.info?.height, + maxHeight = maxHeight, + width = messageContent.info?.width, + maxWidth = maxWidth, + allowNonMxcUrls = informationData.sendState.isSending() ) return MessageImageVideoItem_() - .attributes(attributes) - .leftGuideline(avatarSizeProvider.leftGuideline) - .imageContentRenderer(imageContentRenderer) - .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) - .playable(messageContent.mimeType == MimeTypes.Gif) - .highlighted(highlight) - .mediaData(data) - .apply { - if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) { - mode(ImageContentRenderer.Mode.STICKER) - } else { - clickListener { view -> - callback?.onImageMessageClicked(messageContent, data, view) - } + .attributes(attributes) + .leftGuideline(avatarSizeProvider.leftGuideline) + .imageContentRenderer(imageContentRenderer) + .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) + .playable(messageContent.mimeType == MimeTypes.Gif) + .highlighted(highlight) + .mediaData(data) + .apply { + if (messageContent.msgType == MessageType.MSGTYPE_STICKER_LOCAL) { + mode(ImageContentRenderer.Mode.STICKER) + } else { + clickListener { view -> + callback?.onImageMessageClicked(messageContent, data, view) } } + } } - private fun buildVideoMessageItem(messageContent: MessageVideoContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): MessageImageVideoItem? { + private fun buildVideoMessageItem( + messageContent: MessageVideoContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): MessageImageVideoItem? { val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val thumbnailData = ImageContentRenderer.Data( - eventId = informationData.eventId, - filename = messageContent.body, - mimeType = messageContent.mimeType, - url = messageContent.videoInfo?.getThumbnailUrl(), - elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), - height = messageContent.videoInfo?.height, - maxHeight = maxHeight, - width = messageContent.videoInfo?.width, - maxWidth = maxWidth, - allowNonMxcUrls = informationData.sendState.isSending() + eventId = informationData.eventId, + filename = messageContent.body, + mimeType = messageContent.mimeType, + url = messageContent.videoInfo?.getThumbnailUrl(), + elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), + height = messageContent.videoInfo?.height, + maxHeight = maxHeight, + width = messageContent.videoInfo?.width, + maxWidth = maxWidth, + allowNonMxcUrls = informationData.sendState.isSending() ) val videoData = VideoContentRenderer.Data( - eventId = informationData.eventId, - filename = messageContent.body, - mimeType = messageContent.mimeType, - url = messageContent.getFileUrl(), - elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), - thumbnailMediaData = thumbnailData + eventId = informationData.eventId, + filename = messageContent.body, + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), + thumbnailMediaData = thumbnailData ) return MessageImageVideoItem_() - .leftGuideline(avatarSizeProvider.leftGuideline) - .attributes(attributes) - .imageContentRenderer(imageContentRenderer) - .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) - .playable(true) - .highlighted(highlight) - .mediaData(thumbnailData) - .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) } + .leftGuideline(avatarSizeProvider.leftGuideline) + .attributes(attributes) + .imageContentRenderer(imageContentRenderer) + .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) + .playable(true) + .highlighted(highlight) + .mediaData(thumbnailData) + .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) } } - private fun buildItemForTextContent(messageContent: MessageTextContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { + private fun buildItemForTextContent( + messageContent: MessageTextContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): VectorEpoxyModel<*>? { val matrixFormattedBody = messageContent.matrixFormattedBody return if (matrixFormattedBody != null) { buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes) @@ -539,50 +576,56 @@ class MessageItemFactory @Inject constructor( } } - private fun buildFormattedTextItem(matrixFormattedBody: String, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): MessageTextItem? { + private fun buildFormattedTextItem( + matrixFormattedBody: String, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): MessageTextItem? { val compressed = htmlCompressor.compress(matrixFormattedBody) val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes) } - private fun buildMessageTextItem(body: CharSequence, - isFormatted: Boolean, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): MessageTextItem? { + private fun buildMessageTextItem( + body: CharSequence, + isFormatted: Boolean, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): MessageTextItem? { val renderedBody = textRenderer.render(body) val bindingOptions = spanUtils.getBindingOptions(renderedBody) val linkifiedBody = renderedBody.linkify(callback) return MessageTextItem_() - .message( - if (informationData.hasBeenEdited) { - annotateWithEdited(linkifiedBody, callback, informationData) - } else { - linkifiedBody - }.toEpoxyCharSequence() - ) - .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) - .bindingOptions(bindingOptions) - .markwonPlugins(htmlRenderer.get().plugins) - .searchForPills(isFormatted) - .previewUrlRetriever(callback?.getPreviewUrlRetriever()) - .imageContentRenderer(imageContentRenderer) - .previewUrlCallback(callback) - .leftGuideline(avatarSizeProvider.leftGuideline) - .attributes(attributes) - .highlighted(highlight) - .movementMethod(createLinkMovementMethod(callback)) + .message( + if (informationData.hasBeenEdited) { + annotateWithEdited(linkifiedBody, callback, informationData) + } else { + linkifiedBody + }.toEpoxyCharSequence() + ) + .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) + .bindingOptions(bindingOptions) + .markwonPlugins(htmlRenderer.get().plugins) + .searchForPills(isFormatted) + .previewUrlRetriever(callback?.getPreviewUrlRetriever()) + .imageContentRenderer(imageContentRenderer) + .previewUrlCallback(callback) + .leftGuideline(avatarSizeProvider.leftGuideline) + .attributes(attributes) + .highlighted(highlight) + .movementMethod(createLinkMovementMethod(callback)) } - private fun annotateWithEdited(linkifiedBody: CharSequence, - callback: TimelineEventController.Callback?, - informationData: MessageInformationData): Spannable { + private fun annotateWithEdited( + linkifiedBody: CharSequence, + callback: TimelineEventController.Callback?, + informationData: MessageInformationData, + ): Spannable { val spannable = SpannableStringBuilder() spannable.append(linkifiedBody) val editedSuffix = stringProvider.getString(R.string.edited_suffix) @@ -591,17 +634,17 @@ class MessageItemFactory @Inject constructor( val editStart = spannable.lastIndexOf(editedSuffix) val editEnd = editStart + editedSuffix.length spannable.setSpan( - ForegroundColorSpan(color), - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + ForegroundColorSpan(color), + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) // Note: text size is set to 14sp spannable.setSpan( - AbsoluteSizeSpan(dimensionConverter.spToPx(13)), - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + AbsoluteSizeSpan(dimensionConverter.spToPx(13)), + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) spannable.setSpan(object : ClickableSpan() { override fun onClick(widget: View) { @@ -612,18 +655,20 @@ class MessageItemFactory @Inject constructor( // nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } - private fun buildNoticeMessageItem(messageContent: MessageNoticeContent, - @Suppress("UNUSED_PARAMETER") - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): MessageTextItem? { + private fun buildNoticeMessageItem( + messageContent: MessageNoticeContent, + @Suppress("UNUSED_PARAMETER") + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): MessageTextItem? { val htmlBody = messageContent.getHtmlBody() val formattedBody = span { text = htmlBody @@ -635,22 +680,24 @@ class MessageItemFactory @Inject constructor( val message = formattedBody.linkify(callback) return MessageTextItem_() - .leftGuideline(avatarSizeProvider.leftGuideline) - .previewUrlRetriever(callback?.getPreviewUrlRetriever()) - .imageContentRenderer(imageContentRenderer) - .previewUrlCallback(callback) - .attributes(attributes) - .message(message.toEpoxyCharSequence()) - .bindingOptions(bindingOptions) - .highlighted(highlight) - .movementMethod(createLinkMovementMethod(callback)) + .leftGuideline(avatarSizeProvider.leftGuideline) + .previewUrlRetriever(callback?.getPreviewUrlRetriever()) + .imageContentRenderer(imageContentRenderer) + .previewUrlCallback(callback) + .attributes(attributes) + .message(message.toEpoxyCharSequence()) + .bindingOptions(bindingOptions) + .highlighted(highlight) + .movementMethod(createLinkMovementMethod(callback)) } - private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): MessageTextItem? { + private fun buildEmoteMessageItem( + messageContent: MessageEmoteContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): MessageTextItem? { val formattedBody = SpannableStringBuilder() formattedBody.append("* ${informationData.memberName} ") formattedBody.append(messageContent.getHtmlBody()) @@ -658,46 +705,48 @@ class MessageItemFactory @Inject constructor( val message = formattedBody.linkify(callback) return MessageTextItem_() - .message( - if (informationData.hasBeenEdited) { - annotateWithEdited(message, callback, informationData) - } else { - message - }.toEpoxyCharSequence() - ) - .bindingOptions(bindingOptions) - .leftGuideline(avatarSizeProvider.leftGuideline) - .previewUrlRetriever(callback?.getPreviewUrlRetriever()) - .imageContentRenderer(imageContentRenderer) - .previewUrlCallback(callback) - .attributes(attributes) - .highlighted(highlight) - .movementMethod(createLinkMovementMethod(callback)) + .message( + if (informationData.hasBeenEdited) { + annotateWithEdited(message, callback, informationData) + } else { + message + }.toEpoxyCharSequence() + ) + .bindingOptions(bindingOptions) + .leftGuideline(avatarSizeProvider.leftGuideline) + .previewUrlRetriever(callback?.getPreviewUrlRetriever()) + .imageContentRenderer(imageContentRenderer) + .previewUrlCallback(callback) + .attributes(attributes) + .highlighted(highlight) + .movementMethod(createLinkMovementMethod(callback)) } private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence { return matrixFormattedBody - ?.let { htmlCompressor.compress(it) } - ?.let { htmlRenderer.get().render(it, pillsPostProcessor) } + ?.let { htmlCompressor.compress(it) } + ?.let { htmlRenderer.get().render(it, pillsPostProcessor) } ?: body } - private fun buildRedactedItem(attributes: AbsMessageItem.Attributes, - highlight: Boolean): RedactedMessageItem? { + private fun buildRedactedItem( + attributes: AbsMessageItem.Attributes, + highlight: Boolean, + ): RedactedMessageItem? { return RedactedMessageItem_() - .layout(attributes.informationData.messageLayout.layoutRes) - .leftGuideline(avatarSizeProvider.leftGuideline) - .attributes(attributes) - .highlighted(highlight) + .layout(attributes.informationData.messageLayout.layoutRes) + .leftGuideline(avatarSizeProvider.leftGuideline) + .attributes(attributes) + .highlighted(highlight) } private fun List?.toFft(): List? { return this - ?.filterNotNull() - ?.map { - // Value comes from AudioRecordView.maxReportableAmp, and 1024 is the max value in the Matrix spec - it * 22760 / 1024 - } + ?.filterNotNull() + ?.map { + // Value comes from AudioRecordView.maxReportableAmp, and 1024 is the max value in the Matrix spec + it * 22760 / 1024 + } } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 0ce225b21c..273dd0299a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -39,7 +39,7 @@ abstract class PollItem : AbsMessageItem() { var eventId: String? = null @EpoxyAttribute - var pollSent: Boolean = false + var canVote: Boolean = false @EpoxyAttribute var totalVotesText: String? = null @@ -80,13 +80,11 @@ abstract class PollItem : AbsMessageItem() { private fun onPollItemClick(optionViewState: PollOptionViewState) { val relatedEventId = eventId - if (isPollActive(optionViewState) && relatedEventId != null) { + if (canVote && relatedEventId != null) { callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId)) } } - private fun isPollActive(optionViewState: PollOptionViewState) = optionViewState !is PollOptionViewState.PollEnded - class Holder : AbsMessageItem.Holder(STUB_ID) { val questionTextView by bind(R.id.questionTextView) val optionsContainer by bind(R.id.optionsContainer) diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index cc02687d93..41add7be7d 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -77,7 +77,7 @@ import im.vector.app.features.pin.PinArgs import im.vector.app.features.pin.PinMode import im.vector.app.features.poll.create.CreatePollActivity import im.vector.app.features.poll.create.CreatePollArgs -import im.vector.app.features.poll.create.PollMode +import im.vector.app.features.poll.PollMode import im.vector.app.features.roomdirectory.RoomDirectoryActivity import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index a31dc8fb89..85826fad5b 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -31,7 +31,7 @@ import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.login.LoginConfig import im.vector.app.features.media.AttachmentData import im.vector.app.features.pin.PinMode -import im.vector.app.features.poll.create.PollMode +import im.vector.app.features.poll.PollMode import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData import im.vector.app.features.settings.VectorSettingsActivity diff --git a/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt b/vector/src/main/java/im/vector/app/features/poll/PollMode.kt similarity index 93% rename from vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt rename to vector/src/main/java/im/vector/app/features/poll/PollMode.kt index 0007589d10..47558a34a4 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/PollMode.kt +++ b/vector/src/main/java/im/vector/app/features/poll/PollMode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.poll.create +package im.vector.app.features.poll enum class PollMode { CREATE, diff --git a/vector/src/main/java/im/vector/app/features/poll/PollState.kt b/vector/src/main/java/im/vector/app/features/poll/PollState.kt new file mode 100644 index 0000000000..076fcad388 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/poll/PollState.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 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.app.features.poll + +sealed class PollState { + object Sending : PollState() + object Ready : PollState() + data class Voted(val votes: Int) : PollState() + object Undisclosed : PollState() + object Ended : PollState() + + fun isVotable() = this !is Sending && this !is Ended +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt index 4483b00158..c00c1ece0c 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollFragment.kt @@ -30,6 +30,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentCreatePollBinding +import im.vector.app.features.poll.PollMode import im.vector.app.features.poll.create.CreatePollViewModel.Companion.MAX_OPTIONS_COUNT import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.room.model.message.PollType diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt index 5c7ef72297..0c822f58fa 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt @@ -23,6 +23,7 @@ import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.poll.PollMode import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.PollType diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt index 175d1b0116..75dbb9ba4d 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt @@ -17,22 +17,16 @@ package im.vector.app.features.poll.create import com.airbnb.mvrx.MavericksState +import im.vector.app.features.poll.PollMode import org.matrix.android.sdk.api.session.room.model.message.PollType data class CreatePollViewState( - val roomId: String, - val editedEventId: String?, - val mode: PollMode, - val question: String = "", - val options: List = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" }, - val canCreatePoll: Boolean = false, - val canAddMoreOptions: Boolean = true, - val pollType: PollType = PollType.DISCLOSED -) : MavericksState { - - constructor(args: CreatePollArgs) : this( - roomId = args.roomId, - editedEventId = args.editedEventId, - mode = args.mode - ) -} + val roomId: String, + val editedEventId: String?, + val mode: PollMode, + val question: String = "", + val options: List = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" }, + val canCreatePoll: Boolean = false, + val canAddMoreOptions: Boolean = true, + val pollType: PollType = PollType.DISCLOSED +) : MavericksState From 86765c9020f02497fa3975c1d91ef9c2e7235187 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Sun, 13 Mar 2022 20:22:03 +0100 Subject: [PATCH 09/27] Adds new line at the end of PollState --- vector/src/main/java/im/vector/app/features/poll/PollState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/poll/PollState.kt b/vector/src/main/java/im/vector/app/features/poll/PollState.kt index 076fcad388..3352c3ad05 100644 --- a/vector/src/main/java/im/vector/app/features/poll/PollState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/PollState.kt @@ -24,4 +24,4 @@ sealed class PollState { object Ended : PollState() fun isVotable() = this !is Sending && this !is Ended -} \ No newline at end of file +} From 06e0047c228b872658df6e8f268f37f66e41b348 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Sun, 13 Mar 2022 20:22:44 +0100 Subject: [PATCH 10/27] Fixes import ordering in DefaultNavigator --- .../java/im/vector/app/features/navigation/DefaultNavigator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 41add7be7d..1dabdb5bf8 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -75,9 +75,9 @@ import im.vector.app.features.onboarding.OnboardingActivity import im.vector.app.features.pin.PinActivity import im.vector.app.features.pin.PinArgs import im.vector.app.features.pin.PinMode +import im.vector.app.features.poll.PollMode import im.vector.app.features.poll.create.CreatePollActivity import im.vector.app.features.poll.create.CreatePollArgs -import im.vector.app.features.poll.PollMode import im.vector.app.features.roomdirectory.RoomDirectoryActivity import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity From 98b7d194eb47208a788b682fd714fa81f5c77c60 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 15 Mar 2022 15:54:30 +0100 Subject: [PATCH 11/27] Adds getBestX following merge from develop --- .../room/detail/timeline/factory/MessageItemFactory.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index ad5aeb98d0..3698b45b19 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -258,9 +258,10 @@ class MessageItemFactory @Inject constructor( ): PollItem { val pollResponseSummary = informationData.pollResponseAggregatedSummary val pollState = createPollState(informationData, pollResponseSummary, pollContent) - val optionViewStates = pollContent.pollCreationInfo?.answers?.mapToOptions(pollState, informationData) - val questionText = pollContent.pollCreationInfo?.question?.question.orEmpty() + val pollCreationInfo = pollContent.getBestPollCreationInfo() + val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() val question = createPollQuestion(informationData, questionText, callback) + val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData) val totalVotesText = createTotalVotesText(pollState, pollResponseSummary) return PollItem_() @@ -283,7 +284,7 @@ class MessageItemFactory @Inject constructor( ): PollState = when { !informationData.sendState.isSent() -> Sending pollResponseSummary?.isClosed.orFalse() -> Ended - pollContent.pollCreationInfo?.kind == PollType.UNDISCLOSED -> Undisclosed + pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> Undisclosed pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> Voted(pollResponseSummary?.totalVotes ?: 0) else -> Ready } @@ -295,7 +296,7 @@ class MessageItemFactory @Inject constructor( val pollResponseSummary = informationData.pollResponseAggregatedSummary val winnerVoteCount = pollResponseSummary?.winnerVoteCount val optionId = answer.id ?: "" - val optionAnswer = answer.answer ?: "" + val optionAnswer = answer.getBestAnswer() ?: "" val voteSummary = pollResponseSummary?.votes?.get(answer.id) val voteCount = voteSummary?.total ?: 0 val votePercentage = voteSummary?.percentage ?: 0.0 From a2d18d460a95c6030d010d345355a885f3d36fa6 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 15 Mar 2022 18:10:05 +0100 Subject: [PATCH 12/27] Fixes PollMode import error in TimelineFragment --- .../im/vector/app/features/home/room/detail/TimelineFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index c638666cd7..0f4c83003a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -182,7 +182,7 @@ import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.PermalinkHandler -import im.vector.app.features.poll.create.PollMode +import im.vector.app.features.poll.PollMode import im.vector.app.features.reactions.EmojiReactionPickerActivity import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.session.coroutineScope From a46901ad6c2c939aab9507988da3f4bc80a0354c Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Tue, 15 Mar 2022 18:10:47 +0100 Subject: [PATCH 13/27] Makes PollState a sealed interface --- .../java/im/vector/app/features/poll/PollState.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/poll/PollState.kt b/vector/src/main/java/im/vector/app/features/poll/PollState.kt index 3352c3ad05..93cdb0ecbe 100644 --- a/vector/src/main/java/im/vector/app/features/poll/PollState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/PollState.kt @@ -16,12 +16,12 @@ package im.vector.app.features.poll -sealed class PollState { - object Sending : PollState() - object Ready : PollState() - data class Voted(val votes: Int) : PollState() - object Undisclosed : PollState() - object Ended : PollState() +sealed interface PollState { + object Sending : PollState + object Ready : PollState + data class Voted(val votes: Int) : PollState + object Undisclosed : PollState + object Ended : PollState fun isVotable() = this !is Sending && this !is Ended } From d11fc060ee28f36333c1dc2bb27d3d9ade87687b Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Thu, 17 Mar 2022 13:46:16 +0100 Subject: [PATCH 14/27] Fixes crash due to empty constructor in CreatePollViewState --- .../app/features/poll/create/CreatePollViewState.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt index 3045bc695a..3fd8ab605c 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt @@ -29,4 +29,11 @@ data class CreatePollViewState( val canCreatePoll: Boolean = false, val canAddMoreOptions: Boolean = true, val pollType: PollType = PollType.DISCLOSED_UNSTABLE -) : MavericksState +) : MavericksState { + + constructor(args: CreatePollArgs) : this( + roomId = args.roomId, + editedEventId = args.editedEventId, + mode = args.mode + ) +} From fbb6f117d048757f3b1f83565cde8bd7f7ec0970 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Thu, 17 Mar 2022 13:47:57 +0100 Subject: [PATCH 15/27] Fixes remote echo of end poll not processing correctly --- .../EventRelationsAggregationProcessor.kt | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index bf01acaccd..8bbe3a9ac6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -398,7 +398,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } val closedTime = existingPollSummary?.closedTime - if (closedTime != null) { + if (closedTime != null && eventTimestamp > closedTime) { Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}") return } @@ -482,46 +482,39 @@ internal class EventRelationsAggregationProcessor @Inject constructor( roomId: String, isLocalEcho: Boolean) { val pollEventId = content.relatesTo?.eventId ?: return - val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId val isPollOwner = pollOwnerId == event.senderId - val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) ?.content?.toModel() ?.let { PowerLevelsHelper(it) } + if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) { Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId") return } - var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() - if (existing == null) { + var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() + if (existingPoll == null) { Timber.v("## POLL creating new relation summary for $pollEventId") - existing = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId) + existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId) } // we have it - val existingPollSummary = existing.pollResponseSummary + val existingPollSummary = existingPoll.pollResponseSummary ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { - existing.pollResponseSummary = it + existingPoll.pollResponseSummary = it } - if (existingPollSummary.closedTime != null) { - Timber.v("## Received poll.end event for already ended poll $pollEventId") - return - } - val txId = event.unsignedData?.transactionId + existingPollSummary.closedTime = event.originServerTs + // is it a remote echo? if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { // ok it has already been managed Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId") existingPollSummary.sourceLocalEchoEvents.remove(txId) existingPollSummary.sourceEvents.add(event.eventId) - return } - - existingPollSummary.closedTime = event.originServerTs } private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? { From 4ef0bc9052480fe1cc0db580b918ad6fda3989d1 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 24 Mar 2022 10:19:53 +0000 Subject: [PATCH 16/27] fixing wrong account created flag when creating a session from a direct login --- .../im/vector/app/features/onboarding/OnboardingViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 6659058b4e..64fa3f0eee 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -578,7 +578,7 @@ class OnboardingViewModel @AssistedInject constructor( onDirectLoginError(failure) return } - onSessionCreated(data, isAccountCreated = true) + onSessionCreated(data, isAccountCreated = false) } private fun onDirectLoginError(failure: Throwable) { From 1a0bd3f31eb932ce45548d81a009935039f36f08 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 24 Mar 2022 15:27:35 +0100 Subject: [PATCH 17/27] Revert "Revert "Do not suggest collapse if there is only one section"" This reverts commit 55b1a60f96e58491ae9c31f7b8453c951a940559. --- .../features/home/room/list/RoomListFragment.kt | 14 +++++++++++--- .../home/room/list/SectionHeaderAdapter.kt | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 4265eebe62..a8f1174283 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -295,7 +295,8 @@ class RoomListFragment @Inject constructor( section.notificationCount.observe(viewLifecycleOwner) { counts -> sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( notificationCount = counts.totalCount, - isHighlighted = counts.isHighlight + isHighlighted = counts.isHighlight, + shouldShowExpandedArrow = shouldShowExpendedArrow() )) } section.isExpanded.observe(viewLifecycleOwner) { _ -> @@ -329,14 +330,17 @@ class RoomListFragment @Inject constructor( controller.setData(list) sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( isHidden = list.isEmpty(), - isLoading = false)) + isLoading = false, + shouldShowExpandedArrow = shouldShowExpendedArrow() + )) checkEmptyState() } observeItemCount(section, sectionAdapter) section.notificationCount.observe(viewLifecycleOwner) { counts -> sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( notificationCount = counts.totalCount, - isHighlighted = counts.isHighlight + isHighlighted = counts.isHighlight, + shouldShowExpandedArrow = shouldShowExpendedArrow() )) } section.isExpanded.observe(viewLifecycleOwner) { _ -> @@ -444,6 +448,10 @@ class RoomListFragment @Inject constructor( footerController.setData(state) } + private fun shouldShowExpendedArrow(): Boolean { + return adapterInfosList.filter { !it.sectionHeaderAdapter.roomsSectionData.isHidden }.size >= 2 + } + private fun checkEmptyState() { val shouldShowEmpty = adapterInfosList.all { it.sectionHeaderAdapter.roomsSectionData.isHidden } && !adapterInfosList.any { it.sectionHeaderAdapter.roomsSectionData.isLoading } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt index 2e6436d21d..cd2879cf28 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.list import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat @@ -39,7 +40,8 @@ class SectionHeaderAdapter constructor( val isHighlighted: Boolean = false, val isHidden: Boolean = true, // This will be false until real data has been submitted once - val isLoading: Boolean = true + val isLoading: Boolean = true, + val shouldShowExpandedArrow: Boolean = false ) lateinit var roomsSectionData: RoomsSectionData @@ -82,11 +84,16 @@ class SectionHeaderAdapter constructor( fun bind(roomsSectionData: RoomsSectionData) { binding.roomCategoryTitleView.text = roomsSectionData.name val tintColor = ThemeUtils.getColor(binding.root.context, R.attr.vctr_content_secondary) - val expandedArrowDrawableRes = if (roomsSectionData.isExpanded) R.drawable.ic_expand_more else R.drawable.ic_expand_less - val expandedArrowDrawable = ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also { - DrawableCompat.setTint(it, tintColor) + if (roomsSectionData.shouldShowExpandedArrow) { + binding.roomCategoryCounterView.visibility = View.VISIBLE + val expandedArrowDrawableRes = if (roomsSectionData.isExpanded) R.drawable.ic_expand_more else R.drawable.ic_expand_less + val expandedArrowDrawable = ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also { + DrawableCompat.setTint(it, tintColor) + } + binding.roomCategoryCounterView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) + } else { + binding.roomCategoryCounterView.visibility = View.GONE } - binding.roomCategoryCounterView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) binding.roomCategoryCounterView.text = roomsSectionData.itemCount.takeIf { it > 0 }?.toString().orEmpty() binding.roomCategoryUnreadCounterBadgeView.render(UnreadCounterBadgeView.State(roomsSectionData.notificationCount, roomsSectionData.isHighlighted)) } From 70e5698082ba472e9c104b19e3802c67c039f82f Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 24 Mar 2022 15:41:35 +0100 Subject: [PATCH 18/27] Update versions to 1.4.7 --- matrix-sdk-android/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 2b2c38e22a..a26a709afc 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -31,7 +31,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.4.6\"" + buildConfigField "String", "SDK_VERSION", "\"1.4.7\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/vector/build.gradle b/vector/build.gradle index aeaad19e02..f58b7d444d 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -18,7 +18,7 @@ ext.versionMinor = 4 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 6 +ext.versionPatch = 7 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From 745382cdfad3105e4bc473cbc7e3cc116eeebf9a Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 24 Mar 2022 15:41:42 +0100 Subject: [PATCH 19/27] RoomList : avoid using flow extension on realm results (leads to frozen object and leaks). --- .../sdk/api/session/room/RoomService.kt | 4 ++-- .../session/room/DefaultRoomService.kt | 4 ++-- .../room/summary/RoomSummaryDataSource.kt | 22 +++++++------------ .../room/list/RoomListSectionBuilderGroup.kt | 4 ++-- .../room/list/RoomListSectionBuilderSpace.kt | 4 ++-- .../spaces/manage/SpaceAddRoomsViewModel.kt | 6 ++--- 6 files changed, 19 insertions(+), 25 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index f506b147df..c2f8e6d0be 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -218,9 +218,9 @@ interface RoomService { sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult /** - * Retrieve a flow on the number of rooms. + * Retrieve a LiveData on the number of rooms. */ - fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow + fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData /** * TODO Doc diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 0d78489fbd..2869628576 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -110,8 +110,8 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder) } - override fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow { - return roomSummaryDataSource.getCountFlow(queryParams) + override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData { + return roomSummaryDataSource.getCountLive(queryParams) } override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index ea4f102fa5..f69fdb37c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -25,12 +25,7 @@ import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmQuery -import io.realm.kotlin.toFlow import io.realm.kotlin.where -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter @@ -241,15 +236,14 @@ internal class RoomSummaryDataSource @Inject constructor( } } - fun getCountFlow(queryParams: RoomSummaryQueryParams): Flow = - realmSessionProvider - .withRealm { realm -> roomSummariesQuery(realm, queryParams).findAllAsync() } - .toFlow() - // need to create the flow on a context dispatcher with a thread with attached Looper - .flowOn(coroutineDispatchers.main) - .map { it.size } - .flowOn(coroutineDispatchers.io) - .distinctUntilChanged() + fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData { + val liveRooms = monarchy.findAllManagedWithChanges { + roomSummariesQuery(it, queryParams) + } + return Transformations.map(liveRooms) { + it.realmResults.count() + } + } fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { var notificationCount: RoomAggregateNotificationCount? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt index ec7915ba34..d687c68092 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt @@ -75,7 +75,7 @@ class RoomListSectionBuilderGroup( onUpdatable(updatableFilterLivePageResult) val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() - .flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) } + .flatMapLatest { session.getRoomCountLive(updatableFilterLivePageResult.queryParams).asFlow() } .distinctUntilChanged() sections.add( @@ -276,7 +276,7 @@ class RoomListSectionBuilderGroup( sectionName = name, livePages = livePagedList, notifyOfLocalEcho = notifyOfLocalEcho, - itemCount = session.getRoomCountFlow(roomQueryParams) + itemCount = session.getRoomCountLive(roomQueryParams).asFlow() ) ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt index f82dbd43e1..3e21b0f58f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt @@ -94,7 +94,7 @@ class RoomListSectionBuilderSpace( onUpdatable(updatableFilterLivePageResult) val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() - .flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) } + .flatMapLatest { session.getRoomCountLive(updatableFilterLivePageResult.queryParams).asFlow() } .distinctUntilChanged() sections.add( @@ -400,7 +400,7 @@ class RoomListSectionBuilderSpace( val itemCountFlow = livePagedList.asFlow() .flatMapLatest { val queryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()) - session.getRoomCountFlow(queryParams) + session.getRoomCountLive(queryParams).asFlow() } .distinctUntilChanged() diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt index 7d99c53f23..318f701985 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt @@ -84,7 +84,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( val spaceCountFlow: Flow by lazy { spaceUpdatableLivePageResult.livePagedList.asFlow() - .flatMapLatest { session.getRoomCountFlow(spaceUpdatableLivePageResult.queryParams) } + .flatMapLatest { session.getRoomCountLive(spaceUpdatableLivePageResult.queryParams).asFlow() } .distinctUntilChanged() } @@ -110,7 +110,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( val roomCountFlow: Flow by lazy { roomUpdatableLivePageResult.livePagedList.asFlow() - .flatMapLatest { session.getRoomCountFlow(roomUpdatableLivePageResult.queryParams) } + .flatMapLatest { session.getRoomCountLive(roomUpdatableLivePageResult.queryParams).asFlow() } .distinctUntilChanged() } @@ -136,7 +136,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( val dmCountFlow: Flow by lazy { dmUpdatableLivePageResult.livePagedList.asFlow() - .flatMapLatest { session.getRoomCountFlow(dmUpdatableLivePageResult.queryParams) } + .flatMapLatest { session.getRoomCountLive(dmUpdatableLivePageResult.queryParams).asFlow() } .distinctUntilChanged() } From a362d5427d7fd28fb4aca37366ed05d1350ecf7e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 23 Mar 2022 15:18:13 +0100 Subject: [PATCH 20/27] Fix arrow visibility on section header --- .../home/room/list/RoomListFragment.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index a8f1174283..9ed170f7c5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -148,9 +148,10 @@ class RoomListFragment @Inject constructor( } private fun refreshCollapseStates() { + val sectionsCount = adapterInfosList.count { !it.sectionHeaderAdapter.roomsSectionData.isHidden } roomListViewModel.sections.forEachIndexed { index, roomsSection -> val actualBlock = adapterInfosList[index] - val isRoomSectionExpanded = roomsSection.isExpanded.value.orTrue() + val isRoomSectionExpanded = roomsSection.isExpanded.value.orTrue() || sectionsCount == 1 if (actualBlock.section.isExpanded && !isRoomSectionExpanded) { // mark controller as collapsed actualBlock.contentEpoxyController.setCollapsed(true) @@ -162,7 +163,10 @@ class RoomListFragment @Inject constructor( isExpanded = isRoomSectionExpanded ) actualBlock.sectionHeaderAdapter.updateSection( - actualBlock.sectionHeaderAdapter.roomsSectionData.copy(isExpanded = isRoomSectionExpanded) + actualBlock.sectionHeaderAdapter.roomsSectionData.copy( + isExpanded = isRoomSectionExpanded, + shouldShowExpandedArrow = sectionsCount > 1 + ) ) } } @@ -289,6 +293,7 @@ class RoomListFragment @Inject constructor( isHidden = pl.isEmpty(), isLoading = false )) + refreshCollapseStates() checkEmptyState() } observeItemCount(section, sectionAdapter) @@ -296,7 +301,6 @@ class RoomListFragment @Inject constructor( sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( notificationCount = counts.totalCount, isHighlighted = counts.isHighlight, - shouldShowExpandedArrow = shouldShowExpendedArrow() )) } section.isExpanded.observe(viewLifecycleOwner) { _ -> @@ -314,6 +318,7 @@ class RoomListFragment @Inject constructor( isHidden = info.rooms.isEmpty(), isLoading = false )) + refreshCollapseStates() checkEmptyState() } observeItemCount(section, sectionAdapter) @@ -331,16 +336,15 @@ class RoomListFragment @Inject constructor( sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( isHidden = list.isEmpty(), isLoading = false, - shouldShowExpandedArrow = shouldShowExpendedArrow() )) + refreshCollapseStates() checkEmptyState() } observeItemCount(section, sectionAdapter) section.notificationCount.observe(viewLifecycleOwner) { counts -> sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( notificationCount = counts.totalCount, - isHighlighted = counts.isHighlight, - shouldShowExpandedArrow = shouldShowExpendedArrow() + isHighlighted = counts.isHighlight )) } section.isExpanded.observe(viewLifecycleOwner) { _ -> @@ -448,10 +452,6 @@ class RoomListFragment @Inject constructor( footerController.setData(state) } - private fun shouldShowExpendedArrow(): Boolean { - return adapterInfosList.filter { !it.sectionHeaderAdapter.roomsSectionData.isHidden }.size >= 2 - } - private fun checkEmptyState() { val shouldShowEmpty = adapterInfosList.all { it.sectionHeaderAdapter.roomsSectionData.isHidden } && !adapterInfosList.any { it.sectionHeaderAdapter.roomsSectionData.isLoading } From a97d3eae7ee19ab3c083bc457c7d5751c78f448c Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 24 Mar 2022 11:58:17 +0100 Subject: [PATCH 21/27] Pass lambda to updateSection method --- .../home/room/list/RoomListFragment.kt | 74 ++++++++++--------- .../home/room/list/SectionHeaderAdapter.kt | 8 +- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 9ed170f7c5..d92fc0403f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -162,12 +162,12 @@ class RoomListFragment @Inject constructor( actualBlock.section = actualBlock.section.copy( isExpanded = isRoomSectionExpanded ) - actualBlock.sectionHeaderAdapter.updateSection( - actualBlock.sectionHeaderAdapter.roomsSectionData.copy( - isExpanded = isRoomSectionExpanded, - shouldShowExpandedArrow = sectionsCount > 1 - ) - ) + actualBlock.sectionHeaderAdapter.updateSection { + it.copy( + isExpanded = isRoomSectionExpanded, + shouldShowExpandedArrow = sectionsCount > 1 + ) + } } } @@ -276,32 +276,34 @@ class RoomListFragment @Inject constructor( val concatAdapter = ConcatAdapter() roomListViewModel.sections.forEach { section -> - val sectionAdapter = SectionHeaderAdapter { + val sectionAdapter = SectionHeaderAdapter(SectionHeaderAdapter.RoomsSectionData(section.sectionName)) { roomListViewModel.handle(RoomListAction.ToggleSection(section)) - }.also { - it.updateSection(SectionHeaderAdapter.RoomsSectionData(section.sectionName)) } val contentAdapter = when { - section.livePages != null -> { + section.livePages != null -> { pagedControllerFactory.createRoomSummaryPagedController() .also { controller -> section.livePages.observe(viewLifecycleOwner) { pl -> controller.submitList(pl) - sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( - isHidden = pl.isEmpty(), - isLoading = false - )) + sectionAdapter.updateSection { + it.copy( + isHidden = pl.isEmpty(), + isLoading = false + ) + } refreshCollapseStates() checkEmptyState() } observeItemCount(section, sectionAdapter) section.notificationCount.observe(viewLifecycleOwner) { counts -> - sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( - notificationCount = counts.totalCount, - isHighlighted = counts.isHighlight, - )) + sectionAdapter.updateSection { + it.copy( + notificationCount = counts.totalCount, + isHighlighted = counts.isHighlight, + ) + } } section.isExpanded.observe(viewLifecycleOwner) { _ -> refreshCollapseStates() @@ -314,10 +316,12 @@ class RoomListFragment @Inject constructor( .also { controller -> section.liveSuggested.observe(viewLifecycleOwner) { info -> controller.setData(info) - sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( - isHidden = info.rooms.isEmpty(), - isLoading = false - )) + sectionAdapter.updateSection { + it.copy( + isHidden = info.rooms.isEmpty(), + isLoading = false + ) + } refreshCollapseStates() checkEmptyState() } @@ -333,19 +337,23 @@ class RoomListFragment @Inject constructor( .also { controller -> section.liveList?.observe(viewLifecycleOwner) { list -> controller.setData(list) - sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( - isHidden = list.isEmpty(), - isLoading = false, - )) + sectionAdapter.updateSection { + it.copy( + isHidden = list.isEmpty(), + isLoading = false, + ) + } refreshCollapseStates() checkEmptyState() } observeItemCount(section, sectionAdapter) section.notificationCount.observe(viewLifecycleOwner) { counts -> - sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( - notificationCount = counts.totalCount, - isHighlighted = counts.isHighlight - )) + sectionAdapter.updateSection { + it.copy( + notificationCount = counts.totalCount, + isHighlighted = counts.isHighlight + ) + } } section.isExpanded.observe(viewLifecycleOwner) { _ -> refreshCollapseStates() @@ -393,9 +401,9 @@ class RoomListFragment @Inject constructor( section.itemCount .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .collect { count -> - sectionAdapter.updateSection( - sectionAdapter.roomsSectionData.copy(itemCount = count) - ) + sectionAdapter.updateSection { + it.copy(itemCount = count) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt index cd2879cf28..0267151bd1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt @@ -29,6 +29,7 @@ import im.vector.app.databinding.ItemRoomCategoryBinding import im.vector.app.features.themes.ThemeUtils class SectionHeaderAdapter constructor( + roomsSectionData: RoomsSectionData, private val onClickAction: ClickListener ) : RecyclerView.Adapter() { @@ -44,11 +45,12 @@ class SectionHeaderAdapter constructor( val shouldShowExpandedArrow: Boolean = false ) - lateinit var roomsSectionData: RoomsSectionData + var roomsSectionData: RoomsSectionData = roomsSectionData private set - fun updateSection(newRoomsSectionData: RoomsSectionData) { - if (!::roomsSectionData.isInitialized || newRoomsSectionData != roomsSectionData) { + fun updateSection(block: (RoomsSectionData) -> RoomsSectionData) { + val newRoomsSectionData = block(roomsSectionData) + if (roomsSectionData != newRoomsSectionData) { roomsSectionData = newRoomsSectionData notifyDataSetChanged() } From 1ef1bd81bcec155126865d132d4b89e9a248fab5 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 24 Mar 2022 14:35:40 +0100 Subject: [PATCH 22/27] Improve room section collapsing --- .../home/room/list/RoomListFragment.kt | 21 ++++++++++++------- .../home/room/list/SectionHeaderAdapter.kt | 4 ++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index d92fc0403f..1ae4bc135b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -151,7 +151,8 @@ class RoomListFragment @Inject constructor( val sectionsCount = adapterInfosList.count { !it.sectionHeaderAdapter.roomsSectionData.isHidden } roomListViewModel.sections.forEachIndexed { index, roomsSection -> val actualBlock = adapterInfosList[index] - val isRoomSectionExpanded = roomsSection.isExpanded.value.orTrue() || sectionsCount == 1 + val isRoomSectionCollapsable = sectionsCount > 1 + val isRoomSectionExpanded = roomsSection.isExpanded.value.orTrue() if (actualBlock.section.isExpanded && !isRoomSectionExpanded) { // mark controller as collapsed actualBlock.contentEpoxyController.setCollapsed(true) @@ -159,15 +160,18 @@ class RoomListFragment @Inject constructor( // we must expand! actualBlock.contentEpoxyController.setCollapsed(false) } - actualBlock.section = actualBlock.section.copy( - isExpanded = isRoomSectionExpanded - ) + actualBlock.section = actualBlock.section.copy(isExpanded = isRoomSectionExpanded) actualBlock.sectionHeaderAdapter.updateSection { it.copy( isExpanded = isRoomSectionExpanded, - shouldShowExpandedArrow = sectionsCount > 1 + isCollapsable = isRoomSectionCollapsable ) } + + if (!isRoomSectionExpanded && !isRoomSectionCollapsable) { + // force expand if the section is not collapsable + roomListViewModel.handle(RoomListAction.ToggleSection(roomsSection)) + } } } @@ -275,11 +279,12 @@ class RoomListFragment @Inject constructor( val concatAdapter = ConcatAdapter() - roomListViewModel.sections.forEach { section -> + roomListViewModel.sections.forEachIndexed { index, section -> val sectionAdapter = SectionHeaderAdapter(SectionHeaderAdapter.RoomsSectionData(section.sectionName)) { - roomListViewModel.handle(RoomListAction.ToggleSection(section)) + if (adapterInfosList[index].sectionHeaderAdapter.roomsSectionData.isCollapsable) { + roomListViewModel.handle(RoomListAction.ToggleSection(section)) + } } - val contentAdapter = when { section.livePages != null -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt index 0267151bd1..7633bc0d2a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt @@ -42,7 +42,7 @@ class SectionHeaderAdapter constructor( val isHidden: Boolean = true, // This will be false until real data has been submitted once val isLoading: Boolean = true, - val shouldShowExpandedArrow: Boolean = false + val isCollapsable: Boolean = false ) var roomsSectionData: RoomsSectionData = roomsSectionData @@ -86,7 +86,7 @@ class SectionHeaderAdapter constructor( fun bind(roomsSectionData: RoomsSectionData) { binding.roomCategoryTitleView.text = roomsSectionData.name val tintColor = ThemeUtils.getColor(binding.root.context, R.attr.vctr_content_secondary) - if (roomsSectionData.shouldShowExpandedArrow) { + if (roomsSectionData.isCollapsable) { binding.roomCategoryCounterView.visibility = View.VISIBLE val expandedArrowDrawableRes = if (roomsSectionData.isExpanded) R.drawable.ic_expand_more else R.drawable.ic_expand_less val expandedArrowDrawable = ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also { From 3c73ccce7b22f6489c06f014ed2f07e97b5de077 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 24 Mar 2022 14:43:25 +0100 Subject: [PATCH 23/27] Add changelog entry --- changelog.d/5616.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5616.bugfix diff --git a/changelog.d/5616.bugfix b/changelog.d/5616.bugfix new file mode 100644 index 0000000000..5bec45854d --- /dev/null +++ b/changelog.d/5616.bugfix @@ -0,0 +1 @@ +Fix inconsistencies between the arrow visibility and the collapse action on the room sections \ No newline at end of file From 87438085c66483c5858f2c78b2bbf24a0f9a2477 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 24 Mar 2022 18:49:57 +0100 Subject: [PATCH 24/27] RoomList: fix count not showing if not collapsable --- .../features/home/room/list/SectionHeaderAdapter.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt index 7633bc0d2a..3f1dfebf7b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt @@ -16,8 +16,8 @@ package im.vector.app.features.home.room.list +import android.graphics.drawable.Drawable import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat @@ -86,16 +86,16 @@ class SectionHeaderAdapter constructor( fun bind(roomsSectionData: RoomsSectionData) { binding.roomCategoryTitleView.text = roomsSectionData.name val tintColor = ThemeUtils.getColor(binding.root.context, R.attr.vctr_content_secondary) - if (roomsSectionData.isCollapsable) { - binding.roomCategoryCounterView.visibility = View.VISIBLE + val collapsableArrowDrawable: Drawable? = if (roomsSectionData.isCollapsable) { val expandedArrowDrawableRes = if (roomsSectionData.isExpanded) R.drawable.ic_expand_more else R.drawable.ic_expand_less - val expandedArrowDrawable = ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also { + ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also { DrawableCompat.setTint(it, tintColor) } - binding.roomCategoryCounterView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) } else { - binding.roomCategoryCounterView.visibility = View.GONE + null } + binding.root.isClickable = roomsSectionData.isCollapsable + binding.roomCategoryCounterView.setCompoundDrawablesWithIntrinsicBounds(null, null, collapsableArrowDrawable, null) binding.roomCategoryCounterView.text = roomsSectionData.itemCount.takeIf { it > 0 }?.toString().orEmpty() binding.roomCategoryUnreadCounterBadgeView.render(UnreadCounterBadgeView.State(roomsSectionData.notificationCount, roomsSectionData.isHighlighted)) } From 04b136e3e4bccdc5b5ebbd026bc39d14545ac84d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 24 Mar 2022 18:50:33 +0100 Subject: [PATCH 25/27] RoomList: more fixes on count --- .../sdk/api/session/room/RoomService.kt | 4 +- .../session/room/DefaultRoomService.kt | 1 - .../room/summary/RoomSummaryDataSource.kt | 2 +- .../home/room/list/RoomListFragment.kt | 4 +- .../room/list/RoomListSectionBuilderGroup.kt | 80 +++--- .../room/list/RoomListSectionBuilderSpace.kt | 264 +++++++++--------- 6 files changed, 179 insertions(+), 176 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index c2f8e6d0be..aec358218b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData import androidx.paging.PagedList -import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership @@ -218,7 +217,8 @@ interface RoomService { sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult /** - * Retrieve a LiveData on the number of rooms. + * Return a LiveData on the number of rooms + * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance. */ fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 2869628576..1bc98015aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -20,7 +20,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy -import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.RoomService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index f69fdb37c7..1925fe8e9f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -241,7 +241,7 @@ internal class RoomSummaryDataSource @Inject constructor( roomSummariesQuery(it, queryParams) } return Transformations.map(liveRooms) { - it.realmResults.count() + it.realmResults.where().count().toInt() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 1ae4bc135b..67d7a732cc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -53,6 +53,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.home.room.list.widget.NotifsFabMenuView import im.vector.app.features.notifications.NotificationDrawerManager import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -287,7 +288,7 @@ class RoomListFragment @Inject constructor( } val contentAdapter = when { - section.livePages != null -> { + section.livePages != null -> { pagedControllerFactory.createRoomSummaryPagedController() .also { controller -> section.livePages.observe(viewLifecycleOwner) { pl -> @@ -405,6 +406,7 @@ class RoomListFragment @Inject constructor( lifecycleScope.launch { section.itemCount .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .filter { it > 0 } .collect { count -> sectionAdapter.updateSection { it.copy(itemCount = count) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt index d687c68092..bd43a83f2c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt @@ -70,22 +70,20 @@ class RoomListSectionBuilderGroup( }, { qpm -> val name = stringProvider.getString(R.string.bottom_action_rooms) - session.getFilteredPagedRoomSummariesLive(qpm) - .let { updatableFilterLivePageResult -> - onUpdatable(updatableFilterLivePageResult) + val updatableFilterLivePageResult = session.getFilteredPagedRoomSummariesLive(qpm) + onUpdatable(updatableFilterLivePageResult) - val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() - .flatMapLatest { session.getRoomCountLive(updatableFilterLivePageResult.queryParams).asFlow() } - .distinctUntilChanged() + val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() + .flatMapLatest { session.getRoomCountLive(updatableFilterLivePageResult.queryParams).asFlow() } + .distinctUntilChanged() - sections.add( - RoomsSection( - sectionName = name, - livePages = updatableFilterLivePageResult.livePagedList, - itemCount = itemCountFlow - ) - ) - } + sections.add( + RoomsSection( + sectionName = name, + livePages = updatableFilterLivePageResult.livePagedList, + itemCount = itemCountFlow + ) + ) } ) } @@ -252,37 +250,33 @@ class RoomListSectionBuilderGroup( @StringRes nameRes: Int, notifyOfLocalEcho: Boolean = false, query: (RoomSummaryQueryParams.Builder) -> Unit) { - withQueryParams( - { query.invoke(it) }, - { roomQueryParams -> - val name = stringProvider.getString(nameRes) - session.getFilteredPagedRoomSummariesLive(roomQueryParams) - .also { - activeSpaceUpdaters.add(it) - }.livePagedList - .let { livePagedList -> - // use it also as a source to update count - livePagedList.asFlow() - .onEach { - sections.find { it.sectionName == name } - ?.notificationCount - ?.postValue(session.getNotificationCountForRooms(roomQueryParams)) - } - .flowOn(Dispatchers.Default) - .launchIn(coroutineScope) + withQueryParams(query) { roomQueryParams -> + val name = stringProvider.getString(nameRes) + session.getFilteredPagedRoomSummariesLive(roomQueryParams) + .also { + activeSpaceUpdaters.add(it) + }.livePagedList + .let { livePagedList -> + // use it also as a source to update count + livePagedList.asFlow() + .onEach { + sections.find { it.sectionName == name } + ?.notificationCount + ?.postValue(session.getNotificationCountForRooms(roomQueryParams)) + } + .flowOn(Dispatchers.Default) + .launchIn(coroutineScope) - sections.add( - RoomsSection( - sectionName = name, - livePages = livePagedList, - notifyOfLocalEcho = notifyOfLocalEcho, - itemCount = session.getRoomCountLive(roomQueryParams).asFlow() - ) + sections.add( + RoomsSection( + sectionName = name, + livePages = livePagedList, + notifyOfLocalEcho = notifyOfLocalEcho, + itemCount = session.getRoomCountLive(roomQueryParams).asFlow() ) - } - } - - ) + ) + } + } } private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt index 3e21b0f58f..262df2784a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt @@ -32,6 +32,7 @@ import im.vector.app.features.invite.showInvites import im.vector.app.space import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -40,6 +41,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter @@ -83,64 +85,10 @@ class RoomListSectionBuilderSpace( } RoomListDisplayMode.FILTERED -> { // Used when searching for rooms - withQueryParams( - { - it.memberships = Membership.activeMemberships() - }, - { qpm -> - val name = stringProvider.getString(R.string.bottom_action_rooms) - session.getFilteredPagedRoomSummariesLive(qpm) - .let { updatableFilterLivePageResult -> - onUpdatable(updatableFilterLivePageResult) - - val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() - .flatMapLatest { session.getRoomCountLive(updatableFilterLivePageResult.queryParams).asFlow() } - .distinctUntilChanged() - - sections.add( - RoomsSection( - sectionName = name, - livePages = updatableFilterLivePageResult.livePagedList, - itemCount = itemCountFlow - ) - ) - } - } - ) + buildFilteredSection(sections) } RoomListDisplayMode.NOTIFICATIONS -> { - if (autoAcceptInvites.showInvites()) { - addSection( - sections = sections, - activeSpaceUpdaters = activeSpaceAwareQueries, - nameRes = R.string.invitations_header, - notifyOfLocalEcho = true, - spaceFilterStrategy = if (onlyOrphansInHome) { - RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL - } else { - RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL - }, - countRoomAsNotif = true - ) { - it.memberships = listOf(Membership.INVITE) - it.roomCategoryFilter = RoomCategoryFilter.ALL - } - } - - addSection( - sections = sections, - activeSpaceUpdaters = activeSpaceAwareQueries, - nameRes = R.string.bottom_action_rooms, - notifyOfLocalEcho = false, - spaceFilterStrategy = if (onlyOrphansInHome) { - RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL - } else { - RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL - } - ) { - it.memberships = listOf(Membership.JOIN) - it.roomCategoryFilter = RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS - } + buildNotificationsSection(sections, activeSpaceAwareQueries) } } @@ -332,6 +280,67 @@ class RoomListSectionBuilderSpace( } } + private fun buildNotificationsSection(sections: MutableList, activeSpaceAwareQueries: MutableList) { + if (autoAcceptInvites.showInvites()) { + addSection( + sections = sections, + activeSpaceUpdaters = activeSpaceAwareQueries, + nameRes = R.string.invitations_header, + notifyOfLocalEcho = true, + spaceFilterStrategy = if (onlyOrphansInHome) { + RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL + } else { + RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL + }, + countRoomAsNotif = true + ) { + it.memberships = listOf(Membership.INVITE) + it.roomCategoryFilter = RoomCategoryFilter.ALL + } + } + + addSection( + sections = sections, + activeSpaceUpdaters = activeSpaceAwareQueries, + nameRes = R.string.bottom_action_rooms, + notifyOfLocalEcho = false, + spaceFilterStrategy = if (onlyOrphansInHome) { + RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL + } else { + RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL + } + ) { + it.memberships = listOf(Membership.JOIN) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS + } + } + + private fun buildFilteredSection(sections: MutableList) { + // Used when searching for rooms + withQueryParams( + { + it.memberships = Membership.activeMemberships() + }, + { qpm -> + val name = stringProvider.getString(R.string.bottom_action_rooms) + val updatableFilterLivePageResult = session.getFilteredPagedRoomSummariesLive(qpm) + onUpdatable(updatableFilterLivePageResult) + + val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() + .flatMapLatest { session.getRoomCountLive(updatableFilterLivePageResult.queryParams).asFlow() } + .distinctUntilChanged() + + sections.add( + RoomsSection( + sectionName = name, + livePages = updatableFilterLivePageResult.livePagedList, + itemCount = itemCountFlow + ) + ) + } + ) + } + private fun addSection(sections: MutableList, activeSpaceUpdaters: MutableList, @StringRes nameRes: Int, @@ -339,83 +348,82 @@ class RoomListSectionBuilderSpace( spaceFilterStrategy: RoomListViewModel.SpaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.NONE, countRoomAsNotif: Boolean = false, query: (RoomSummaryQueryParams.Builder) -> Unit) { - withQueryParams( - { query.invoke(it) }, - { roomQueryParams -> - val name = stringProvider.getString(nameRes) - session.getFilteredPagedRoomSummariesLive( - roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()), - pagedListConfig - ).also { - when (spaceFilterStrategy) { - RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> { - activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater { - override fun updateForSpaceId(roomId: String?) { - it.queryParams = roomQueryParams.copy( - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) - ) - } - }) - } - RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> { - activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater { - override fun updateForSpaceId(roomId: String?) { - if (roomId != null) { - it.queryParams = roomQueryParams.copy( - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) - ) - } else { - it.queryParams = roomQueryParams.copy( - activeSpaceFilter = ActiveSpaceFilter.None - ) - } - } - }) - } - RoomListViewModel.SpaceFilterStrategy.NONE -> { - // we ignore current space for this one - } + withQueryParams(query) { roomQueryParams -> + val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()) + val liveQueryParams = MutableStateFlow(updatedQueryParams) + val itemCountFlow = liveQueryParams + .flatMapLatest { + session.getRoomCountLive(it).asFlow() + } + .flowOn(Dispatchers.Main) + .distinctUntilChanged() + + val name = stringProvider.getString(nameRes) + val filteredPagedRoomSummariesLive = session.getFilteredPagedRoomSummariesLive( + roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()), + pagedListConfig + ) + when (spaceFilterStrategy) { + RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> { + activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater { + override fun updateForSpaceId(roomId: String?) { + filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy( + activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) + ) + liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams } } - }.livePagedList - .let { livePagedList -> - // use it also as a source to update count - livePagedList.asFlow() - .onEach { - Timber.v("Thread space list: ${Thread.currentThread()}") - sections.find { it.sectionName == name } - ?.notificationCount - ?.postValue( - if (countRoomAsNotif) { - RoomAggregateNotificationCount(it.size, it.size) - } else { - session.getNotificationCountForRooms( - roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()) - ) - } - ) - } - .flowOn(Dispatchers.Default) - .launchIn(viewModelScope) - - val itemCountFlow = livePagedList.asFlow() - .flatMapLatest { - val queryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()) - session.getRoomCountLive(queryParams).asFlow() - } - .distinctUntilChanged() - - sections.add( - RoomsSection( - sectionName = name, - livePages = livePagedList, - notifyOfLocalEcho = notifyOfLocalEcho, - itemCount = itemCountFlow - ) + }) + } + RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL -> { + activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater { + override fun updateForSpaceId(roomId: String?) { + if (roomId != null) { + filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy( + activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) + ) + } else { + filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy( + activeSpaceFilter = ActiveSpaceFilter.None ) } + liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams } + } + }) } + RoomListViewModel.SpaceFilterStrategy.NONE -> { + // we ignore current space for this one + } + } - ) + val livePagedList = filteredPagedRoomSummariesLive.livePagedList + // use it also as a source to update count + livePagedList.asFlow() + .onEach { + Timber.v("Thread space list: ${Thread.currentThread()}") + sections.find { it.sectionName == name } + ?.notificationCount + ?.postValue( + if (countRoomAsNotif) { + RoomAggregateNotificationCount(it.size, it.size) + } else { + session.getNotificationCountForRooms( + roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()) + ) + } + ) + } + .flowOn(Dispatchers.Default) + .launchIn(viewModelScope) + + sections.add( + RoomsSection( + sectionName = name, + livePages = livePagedList, + notifyOfLocalEcho = notifyOfLocalEcho, + itemCount = itemCountFlow + ) + ) + } } private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) { From 8bcc2f5b0c62e27fd87f7374a256cc98cd477287 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 24 Mar 2022 19:07:44 +0100 Subject: [PATCH 26/27] Fix formating --- .../app/features/home/room/list/RoomListSectionBuilderSpace.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt index 262df2784a..d405bc5b6f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt @@ -280,7 +280,8 @@ class RoomListSectionBuilderSpace( } } - private fun buildNotificationsSection(sections: MutableList, activeSpaceAwareQueries: MutableList) { + private fun buildNotificationsSection(sections: MutableList, + activeSpaceAwareQueries: MutableList) { if (autoAcceptInvites.showInvites()) { addSection( sections = sections, From 60ecfd4fc23d37a3a6363010cce52f1a0b34ff5e Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 24 Mar 2022 19:08:55 +0100 Subject: [PATCH 27/27] Update changes --- CHANGES.md | 9 +++++++++ changelog.d/5616.bugfix | 1 - fastlane/metadata/android/en-US/changelogs/40104070.txt | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) delete mode 100644 changelog.d/5616.bugfix create mode 100644 fastlane/metadata/android/en-US/changelogs/40104070.txt diff --git a/CHANGES.md b/CHANGES.md index e293a776dd..5f0a2945de 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +Changes in Element v1.4.7 (2022-03-24) +====================================== + +Bugfixes 🐛 +---------- + - Fix inconsistencies between the arrow visibility and the collapse action on the room sections ([#5616](https://github.com/vector-im/element-android/issues/5616)) + - Fix room list header count flickering + Changes in Element v1.4.6 (2022-03-23) ====================================== @@ -37,6 +45,7 @@ SDK API changes ⚠️ Other changes ------------- + - Refactoring for safer olm and megolm session usage ([#5380](https://github.com/vector-im/element-android/issues/5380)) - Improve headers UI in Rooms/Messages lists ([#4533](https://github.com/vector-im/element-android/issues/4533)) - Number of unread messages on space badge now include number of unread DMs ([#5260](https://github.com/vector-im/element-android/issues/5260)) - Amend spaces menu to be consistent with iOS version ([#5270](https://github.com/vector-im/element-android/issues/5270)) diff --git a/changelog.d/5616.bugfix b/changelog.d/5616.bugfix deleted file mode 100644 index 5bec45854d..0000000000 --- a/changelog.d/5616.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix inconsistencies between the arrow visibility and the collapse action on the room sections \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40104070.txt b/fastlane/metadata/android/en-US/changelogs/40104070.txt new file mode 100644 index 0000000000..99a3ecfe7b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40104070.txt @@ -0,0 +1,2 @@ +Main changes in this version: Various bug fixes and stability improvements. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.7 \ No newline at end of file