Merge remote-tracking branch 'origin/develop' into feature/eric/audio-files-player

# Conflicts:
#	vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
This commit is contained in:
ericdecanini 2022-03-24 21:26:39 +01:00
commit c8a56d63e9
23 changed files with 742 additions and 621 deletions

View file

@ -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) Changes in Element v1.4.6 (2022-03-23)
====================================== ======================================
@ -37,6 +45,7 @@ SDK API changes ⚠️
Other 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)) - 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)) - 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)) - Amend spaces menu to be consistent with iOS version ([#5270](https://github.com/vector-im/element-android/issues/5270))

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

@ -0,0 +1 @@
Fixes polls being votable after being ended

View file

@ -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

View file

@ -18,7 +18,6 @@ package org.matrix.android.sdk.api.session.room
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList 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.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
@ -218,9 +217,10 @@ interface RoomService {
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
/** /**
* Retrieve a flow 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 getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int> fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int>
/** /**
* TODO Doc * TODO Doc

View file

@ -20,7 +20,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.paging.PagedList import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy 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.events.model.Event
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomService
@ -110,8 +109,8 @@ internal class DefaultRoomService @Inject constructor(
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder) return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
} }
override fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int> { override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
return roomSummaryDataSource.getCountFlow(queryParams) return roomSummaryDataSource.getCountLive(queryParams)
} }
override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {

View file

@ -482,46 +482,39 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
roomId: String, roomId: String,
isLocalEcho: Boolean) { isLocalEcho: Boolean) {
val pollEventId = content.relatesTo?.eventId ?: return val pollEventId = content.relatesTo?.eventId ?: return
val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId
val isPollOwner = pollOwnerId == event.senderId val isPollOwner = pollOwnerId == event.senderId
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
?.content?.toModel<PowerLevelsContent>() ?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) } ?.let { PowerLevelsHelper(it) }
if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) { 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") Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId")
return return
} }
var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
if (existing == null) { if (existingPoll == null) {
Timber.v("## POLL creating new relation summary for $pollEventId") Timber.v("## POLL creating new relation summary for $pollEventId")
existing = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId) existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
} }
// we have it // we have it
val existingPollSummary = existing.pollResponseSummary val existingPollSummary = existingPoll.pollResponseSummary
?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { ?: 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 val txId = event.unsignedData?.transactionId
existingPollSummary.closedTime = event.originServerTs
// is it a remote echo? // is it a remote echo?
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
// ok it has already been managed // ok it has already been managed
Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId") Timber.v("## POLL Receiving remote echo of response eventId:$pollEventId")
existingPollSummary.sourceLocalEchoEvents.remove(txId) existingPollSummary.sourceLocalEchoEvents.remove(txId)
existingPollSummary.sourceEvents.add(event.eventId) existingPollSummary.sourceEvents.add(event.eventId)
return
} }
existingPollSummary.closedTime = event.originServerTs
} }
private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? { private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? {

View file

@ -25,12 +25,7 @@ import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.toFlow
import io.realm.kotlin.where 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.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.ActiveSpaceFilter
import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter
@ -241,15 +236,14 @@ internal class RoomSummaryDataSource @Inject constructor(
} }
} }
fun getCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int> = fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
realmSessionProvider val liveRooms = monarchy.findAllManagedWithChanges {
.withRealm { realm -> roomSummariesQuery(realm, queryParams).findAllAsync() } roomSummariesQuery(it, queryParams)
.toFlow() }
// need to create the flow on a context dispatcher with a thread with attached Looper return Transformations.map(liveRooms) {
.flowOn(coroutineDispatchers.main) it.realmResults.where().count().toInt()
.map { it.size } }
.flowOn(coroutineDispatchers.io) }
.distinctUntilChanged()
fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
var notificationCount: RoomAggregateNotificationCount? = null var notificationCount: RoomAggregateNotificationCount? = null

View file

@ -184,7 +184,7 @@ import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.NavigationInterceptor
import im.vector.app.features.permalink.PermalinkHandler 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.reactions.EmojiReactionPickerActivity
import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.roomprofile.RoomProfileActivity
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope

View file

@ -58,7 +58,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.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.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.RedactedMessageItem_ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
@ -75,6 +80,12 @@ import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.location.toLocationData import im.vector.app.features.location.toLocationData
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer 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.app.features.settings.VectorPreferences
import im.vector.app.features.voice.AudioWaveformView import im.vector.app.features.voice.AudioWaveformView
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
@ -98,6 +109,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.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent 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.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.PollType
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
@ -181,8 +193,8 @@ class MessageItemFactory @Inject constructor(
// always hide summary when we are on thread timeline // always hide summary when we are on thread timeline
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, params.reactionsSummaryEvents, threadDetails) val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, params.reactionsSummaryEvents, threadDetails)
// val all = event.root.toContent() // val all = event.root.toContent()
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
val messageItem = when (messageContent) { val messageItem = when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
@ -207,10 +219,12 @@ class MessageItemFactory @Inject constructor(
} }
} }
private fun buildLocationItem(locationContent: MessageLocationContent, private fun buildLocationItem(
locationContent: MessageLocationContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageLocationItem? { attributes: AbsMessageItem.Attributes,
): MessageLocationItem? {
val width = timelineMediaSizeProvider.getMaxSize().first val width = timelineMediaSizeProvider.getMaxSize().first
val height = dimensionConverter.dpToPx(200) val height = dimensionConverter.dpToPx(200)
@ -231,75 +245,26 @@ class MessageItemFactory @Inject constructor(
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
} }
private fun buildPollItem(pollContent: MessagePollContent, private fun buildPollItem(
pollContent: MessagePollContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): PollItem? { attributes: AbsMessageItem.Attributes,
val optionViewStates = mutableListOf<PollOptionViewState>() ): PollItem {
val pollResponseSummary = informationData.pollResponseAggregatedSummary val pollResponseSummary = informationData.pollResponseAggregatedSummary
val isEnded = pollResponseSummary?.isClosed.orFalse() val pollState = createPollState(informationData, pollResponseSummary, pollContent)
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() val pollCreationInfo = pollContent.getBestPollCreationInfo()
val winnerVoteCount = pollResponseSummary?.winnerVoteCount val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty()
val isPollSent = informationData.sendState.isSent() val question = createPollQuestion(informationData, questionText, callback)
val isPollUndisclosed = pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED_UNSTABLE val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData)
val totalVotesText = createTotalVotesText(pollState, pollResponseSummary)
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.getBestPollCreationInfo()?.answers?.forEach { option ->
val voteSummary = pollResponseSummary?.votes?.get(option.id)
val isMyVote = pollResponseSummary?.myVote == option.id
val voteCount = voteSummary?.total ?: 0
val votePercentage = voteSummary?.percentage ?: 0.0
val optionId = option.id ?: ""
val optionAnswer = option.getBestAnswer() ?: ""
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.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
return PollItem_() return PollItem_()
.attributes(attributes) .attributes(attributes)
.eventId(informationData.eventId) .eventId(informationData.eventId)
.pollQuestion( .pollQuestion(question)
if (informationData.hasBeenEdited) { .canVote(pollState.isVotable())
annotateWithEdited(question, callback, informationData)
} else {
question
}.toEpoxyCharSequence()
)
.pollSent(isPollSent)
.totalVotesText(totalVotesText) .totalVotesText(totalVotesText)
.optionViewStates(optionViewStates) .optionViewStates(optionViewStates)
.edited(informationData.hasBeenEdited) .edited(informationData.hasBeenEdited)
@ -308,11 +273,72 @@ class MessageItemFactory @Inject constructor(
.callback(callback) .callback(callback)
} }
private fun buildAudioMessageItem(params: TimelineItemFactoryParams, private fun createPollState(
informationData: MessageInformationData,
pollResponseSummary: PollResponseData?,
pollContent: MessagePollContent,
): PollState = when {
!informationData.sendState.isSent() -> Sending
pollResponseSummary?.isClosed.orFalse() -> Ended
pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> Undisclosed
pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> Voted(pollResponseSummary?.totalVotes ?: 0)
else -> Ready
}
private fun List<PollAnswer>.mapToOptions(
pollState: PollState,
informationData: MessageInformationData,
) = map { answer ->
val pollResponseSummary = informationData.pollResponseAggregatedSummary
val winnerVoteCount = pollResponseSummary?.winnerVoteCount
val optionId = answer.id ?: ""
val optionAnswer = answer.getBestAnswer() ?: ""
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(
params: TimelineItemFactoryParams,
messageContent: MessageAudioContent, messageContent: MessageAudioContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageAudioItem { attributes: AbsMessageItem.Attributes
): MessageAudioItem {
val fileUrl = getAudioFileUrl(messageContent, informationData) val fileUrl = getAudioFileUrl(messageContent, informationData)
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params) val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
@ -351,11 +377,13 @@ class MessageItemFactory @Inject constructor(
} }
} }
private fun buildVoiceMessageItem(params: TimelineItemFactoryParams, private fun buildVoiceMessageItem(
params: TimelineItemFactoryParams,
messageContent: MessageAudioContent, messageContent: MessageAudioContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageVoiceItem { attributes: AbsMessageItem.Attributes
): MessageVoiceItem {
val fileUrl = getAudioFileUrl(messageContent, informationData) val fileUrl = getAudioFileUrl(messageContent, informationData)
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params) val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
@ -386,12 +414,14 @@ class MessageItemFactory @Inject constructor(
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
} }
private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent, private fun buildVerificationRequestMessageItem(
messageContent: MessageVerificationRequestContent,
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VerificationRequestItem? { attributes: AbsMessageItem.Attributes,
): VerificationRequestItem? {
// If this request is not sent by me or sent to me, we should ignore it in timeline // If this request is not sent by me or sent to me, we should ignore it in timeline
val myUserId = session.myUserId val myUserId = session.myUserId
if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) { if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
@ -418,7 +448,7 @@ class MessageItemFactory @Inject constructor(
reactionPillCallback = attributes.reactionPillCallback, reactionPillCallback = attributes.reactionPillCallback,
readReceiptsCallback = attributes.readReceiptsCallback, readReceiptsCallback = attributes.readReceiptsCallback,
emojiTypeFace = attributes.emojiTypeFace, emojiTypeFace = attributes.emojiTypeFace,
reactionsSummaryEvents = attributes.reactionsSummaryEvents reactionsSummaryEvents = attributes.reactionsSummaryEvents,
) )
) )
.callback(callback) .callback(callback)
@ -426,9 +456,11 @@ class MessageItemFactory @Inject constructor(
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
} }
private fun buildFileMessageItem(messageContent: MessageFileContent, private fun buildFileMessageItem(
messageContent: MessageFileContent,
highlight: Boolean, highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageFileItem { attributes: AbsMessageItem.Attributes,
): MessageFileItem {
val mxcUrl = messageContent.getFileUrl() ?: "" val mxcUrl = messageContent.getFileUrl() ?: ""
return MessageFileItem_() return MessageFileItem_()
.attributes(attributes) .attributes(attributes)
@ -443,32 +475,37 @@ class MessageItemFactory @Inject constructor(
.iconRes(R.drawable.ic_paperclip) .iconRes(R.drawable.ic_paperclip)
} }
private fun buildAudioContent(params: TimelineItemFactoryParams, private fun buildAudioContent(
params: TimelineItemFactoryParams,
messageContent: MessageAudioContent, messageContent: MessageAudioContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
attributes: AbsMessageItem.Attributes) = attributes: AbsMessageItem.Attributes,
if (messageContent.voiceMessageIndicator != null) { ) = if (messageContent.voiceMessageIndicator != null) {
buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes) buildVoiceMessageItem(params, messageContent, informationData, highlight, attributes)
} else { } else {
buildAudioMessageItem(params, messageContent, informationData, highlight, attributes) buildAudioMessageItem(params, messageContent, informationData, highlight, attributes)
} }
private fun buildNotHandledMessageItem(messageContent: MessageContent, private fun buildNotHandledMessageItem(
messageContent: MessageContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes)
: MessageTextItem? {
// For compatibility reason we should display the body // For compatibility reason we should display the body
return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
} }
private fun buildImageMessageItem(messageContent: MessageImageInfoContent, private fun buildImageMessageItem(
messageContent: MessageImageInfoContent,
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? { attributes: AbsMessageItem.Attributes,
): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data( val data = ImageContentRenderer.Data(
eventId = informationData.eventId, eventId = informationData.eventId,
@ -504,11 +541,13 @@ class MessageItemFactory @Inject constructor(
} }
} }
private fun buildVideoMessageItem(messageContent: MessageVideoContent, private fun buildVideoMessageItem(
messageContent: MessageVideoContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? { attributes: AbsMessageItem.Attributes,
): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
eventId = informationData.eventId, eventId = informationData.eventId,
@ -543,11 +582,13 @@ class MessageItemFactory @Inject constructor(
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) } .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
} }
private fun buildItemForTextContent(messageContent: MessageTextContent, private fun buildItemForTextContent(
messageContent: MessageTextContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { attributes: AbsMessageItem.Attributes,
): VectorEpoxyModel<*>? {
val matrixFormattedBody = messageContent.matrixFormattedBody val matrixFormattedBody = messageContent.matrixFormattedBody
return if (matrixFormattedBody != null) { return if (matrixFormattedBody != null) {
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes) buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes)
@ -556,22 +597,26 @@ class MessageItemFactory @Inject constructor(
} }
} }
private fun buildFormattedTextItem(matrixFormattedBody: String, private fun buildFormattedTextItem(
matrixFormattedBody: String,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
val compressed = htmlCompressor.compress(matrixFormattedBody) val compressed = htmlCompressor.compress(matrixFormattedBody)
val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned
return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes) return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes)
} }
private fun buildMessageTextItem(body: CharSequence, private fun buildMessageTextItem(
body: CharSequence,
isFormatted: Boolean, isFormatted: Boolean,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
val renderedBody = textRenderer.render(body) val renderedBody = textRenderer.render(body)
val bindingOptions = spanUtils.getBindingOptions(renderedBody) val bindingOptions = spanUtils.getBindingOptions(renderedBody)
val linkifiedBody = renderedBody.linkify(callback) val linkifiedBody = renderedBody.linkify(callback)
@ -597,9 +642,11 @@ class MessageItemFactory @Inject constructor(
.movementMethod(createLinkMovementMethod(callback)) .movementMethod(createLinkMovementMethod(callback))
} }
private fun annotateWithEdited(linkifiedBody: CharSequence, private fun annotateWithEdited(
linkifiedBody: CharSequence,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
informationData: MessageInformationData): Spannable { informationData: MessageInformationData,
): Spannable {
val spannable = SpannableStringBuilder() val spannable = SpannableStringBuilder()
spannable.append(linkifiedBody) spannable.append(linkifiedBody)
val editedSuffix = stringProvider.getString(R.string.edited_suffix) val editedSuffix = stringProvider.getString(R.string.edited_suffix)
@ -635,12 +682,14 @@ class MessageItemFactory @Inject constructor(
return spannable return spannable
} }
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent, private fun buildNoticeMessageItem(
messageContent: MessageNoticeContent,
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
val htmlBody = messageContent.getHtmlBody() val htmlBody = messageContent.getHtmlBody()
val formattedBody = span { val formattedBody = span {
text = htmlBody text = htmlBody
@ -663,11 +712,13 @@ class MessageItemFactory @Inject constructor(
.movementMethod(createLinkMovementMethod(callback)) .movementMethod(createLinkMovementMethod(callback))
} }
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, private fun buildEmoteMessageItem(
messageContent: MessageEmoteContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes,
): MessageTextItem? {
val formattedBody = SpannableStringBuilder() val formattedBody = SpannableStringBuilder()
formattedBody.append("* ${informationData.memberName} ") formattedBody.append("* ${informationData.memberName} ")
formattedBody.append(messageContent.getHtmlBody()) formattedBody.append(messageContent.getHtmlBody())
@ -699,8 +750,10 @@ class MessageItemFactory @Inject constructor(
?: body ?: body
} }
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes, private fun buildRedactedItem(
highlight: Boolean): RedactedMessageItem? { attributes: AbsMessageItem.Attributes,
highlight: Boolean,
): RedactedMessageItem? {
return RedactedMessageItem_() return RedactedMessageItem_()
.layout(attributes.informationData.messageLayout.layoutRes) .layout(attributes.informationData.messageLayout.layoutRes)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)

View file

@ -39,7 +39,7 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
var eventId: String? = null var eventId: String? = null
@EpoxyAttribute @EpoxyAttribute
var pollSent: Boolean = false var canVote: Boolean = false
@EpoxyAttribute @EpoxyAttribute
var totalVotesText: String? = null var totalVotesText: String? = null
@ -54,7 +54,6 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
val relatedEventId = eventId ?: return
renderSendState(holder.view, holder.questionTextView) renderSendState(holder.view, holder.questionTextView)
@ -73,12 +72,18 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
optionViewStates.forEachIndexed { index, optionViewState -> optionViewStates.forEachIndexed { index, optionViewState ->
views.getOrNull(index)?.let { views.getOrNull(index)?.let {
it.render(optionViewState) it.render(optionViewState)
it.setOnClickListener { it.setOnClickListener { onPollItemClick(optionViewState) }
}
}
}
private fun onPollItemClick(optionViewState: PollOptionViewState) {
val relatedEventId = eventId
if (canVote && relatedEventId != null) {
callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId)) callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, optionViewState.optionId))
} }
} }
}
}
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageItem.Holder(STUB_ID) {
val questionTextView by bind<TextView>(R.id.questionTextView) val questionTextView by bind<TextView>(R.id.questionTextView)

View file

@ -52,6 +52,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.home.room.list.widget.NotifsFabMenuView
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -147,8 +148,10 @@ class RoomListFragment @Inject constructor(
} }
private fun refreshCollapseStates() { private fun refreshCollapseStates() {
val sectionsCount = adapterInfosList.count { !it.sectionHeaderAdapter.roomsSectionData.isHidden }
roomListViewModel.sections.forEachIndexed { index, roomsSection -> roomListViewModel.sections.forEachIndexed { index, roomsSection ->
val actualBlock = adapterInfosList[index] val actualBlock = adapterInfosList[index]
val isRoomSectionCollapsable = sectionsCount > 1
val isRoomSectionExpanded = roomsSection.isExpanded.value.orTrue() val isRoomSectionExpanded = roomsSection.isExpanded.value.orTrue()
if (actualBlock.section.isExpanded && !isRoomSectionExpanded) { if (actualBlock.section.isExpanded && !isRoomSectionExpanded) {
// mark controller as collapsed // mark controller as collapsed
@ -157,13 +160,19 @@ class RoomListFragment @Inject constructor(
// we must expand! // we must expand!
actualBlock.contentEpoxyController.setCollapsed(false) actualBlock.contentEpoxyController.setCollapsed(false)
} }
actualBlock.section = actualBlock.section.copy( actualBlock.section = actualBlock.section.copy(isExpanded = isRoomSectionExpanded)
isExpanded = isRoomSectionExpanded actualBlock.sectionHeaderAdapter.updateSection {
) it.copy(
actualBlock.sectionHeaderAdapter.updateSection( isExpanded = isRoomSectionExpanded,
actualBlock.sectionHeaderAdapter.roomsSectionData.copy(isExpanded = isRoomSectionExpanded) isCollapsable = isRoomSectionCollapsable
) )
} }
if (!isRoomSectionExpanded && !isRoomSectionCollapsable) {
// force expand if the section is not collapsable
roomListViewModel.handle(RoomListAction.ToggleSection(roomsSection))
}
}
} }
override fun showFailure(throwable: Throwable) { override fun showFailure(throwable: Throwable) {
@ -270,13 +279,12 @@ class RoomListFragment @Inject constructor(
val concatAdapter = ConcatAdapter() val concatAdapter = ConcatAdapter()
roomListViewModel.sections.forEach { section -> roomListViewModel.sections.forEachIndexed { index, section ->
val sectionAdapter = SectionHeaderAdapter { val sectionAdapter = SectionHeaderAdapter(SectionHeaderAdapter.RoomsSectionData(section.sectionName)) {
if (adapterInfosList[index].sectionHeaderAdapter.roomsSectionData.isCollapsable) {
roomListViewModel.handle(RoomListAction.ToggleSection(section)) roomListViewModel.handle(RoomListAction.ToggleSection(section))
}.also {
it.updateSection(SectionHeaderAdapter.RoomsSectionData(section.sectionName))
} }
}
val contentAdapter = val contentAdapter =
when { when {
section.livePages != null -> { section.livePages != null -> {
@ -284,18 +292,23 @@ class RoomListFragment @Inject constructor(
.also { controller -> .also { controller ->
section.livePages.observe(viewLifecycleOwner) { pl -> section.livePages.observe(viewLifecycleOwner) { pl ->
controller.submitList(pl) controller.submitList(pl)
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( sectionAdapter.updateSection {
it.copy(
isHidden = pl.isEmpty(), isHidden = pl.isEmpty(),
isLoading = false isLoading = false
)) )
}
refreshCollapseStates()
checkEmptyState() checkEmptyState()
} }
observeItemCount(section, sectionAdapter) observeItemCount(section, sectionAdapter)
section.notificationCount.observe(viewLifecycleOwner) { counts -> section.notificationCount.observe(viewLifecycleOwner) { counts ->
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( sectionAdapter.updateSection {
it.copy(
notificationCount = counts.totalCount, notificationCount = counts.totalCount,
isHighlighted = counts.isHighlight isHighlighted = counts.isHighlight,
)) )
}
} }
section.isExpanded.observe(viewLifecycleOwner) { _ -> section.isExpanded.observe(viewLifecycleOwner) { _ ->
refreshCollapseStates() refreshCollapseStates()
@ -308,10 +321,13 @@ class RoomListFragment @Inject constructor(
.also { controller -> .also { controller ->
section.liveSuggested.observe(viewLifecycleOwner) { info -> section.liveSuggested.observe(viewLifecycleOwner) { info ->
controller.setData(info) controller.setData(info)
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( sectionAdapter.updateSection {
it.copy(
isHidden = info.rooms.isEmpty(), isHidden = info.rooms.isEmpty(),
isLoading = false isLoading = false
)) )
}
refreshCollapseStates()
checkEmptyState() checkEmptyState()
} }
observeItemCount(section, sectionAdapter) observeItemCount(section, sectionAdapter)
@ -326,17 +342,23 @@ class RoomListFragment @Inject constructor(
.also { controller -> .also { controller ->
section.liveList?.observe(viewLifecycleOwner) { list -> section.liveList?.observe(viewLifecycleOwner) { list ->
controller.setData(list) controller.setData(list)
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( sectionAdapter.updateSection {
it.copy(
isHidden = list.isEmpty(), isHidden = list.isEmpty(),
isLoading = false)) isLoading = false,
)
}
refreshCollapseStates()
checkEmptyState() checkEmptyState()
} }
observeItemCount(section, sectionAdapter) observeItemCount(section, sectionAdapter)
section.notificationCount.observe(viewLifecycleOwner) { counts -> section.notificationCount.observe(viewLifecycleOwner) { counts ->
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( sectionAdapter.updateSection {
it.copy(
notificationCount = counts.totalCount, notificationCount = counts.totalCount,
isHighlighted = counts.isHighlight isHighlighted = counts.isHighlight
)) )
}
} }
section.isExpanded.observe(viewLifecycleOwner) { _ -> section.isExpanded.observe(viewLifecycleOwner) { _ ->
refreshCollapseStates() refreshCollapseStates()
@ -383,10 +405,11 @@ class RoomListFragment @Inject constructor(
lifecycleScope.launch { lifecycleScope.launch {
section.itemCount section.itemCount
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.filter { it > 0 }
.collect { count -> .collect { count ->
sectionAdapter.updateSection( sectionAdapter.updateSection {
sectionAdapter.roomsSectionData.copy(itemCount = count) it.copy(itemCount = count)
) }
} }
} }
} }

View file

@ -70,12 +70,11 @@ class RoomListSectionBuilderGroup(
}, },
{ qpm -> { qpm ->
val name = stringProvider.getString(R.string.bottom_action_rooms) val name = stringProvider.getString(R.string.bottom_action_rooms)
session.getFilteredPagedRoomSummariesLive(qpm) val updatableFilterLivePageResult = session.getFilteredPagedRoomSummariesLive(qpm)
.let { updatableFilterLivePageResult ->
onUpdatable(updatableFilterLivePageResult) onUpdatable(updatableFilterLivePageResult)
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
.flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) } .flatMapLatest { session.getRoomCountLive(updatableFilterLivePageResult.queryParams).asFlow() }
.distinctUntilChanged() .distinctUntilChanged()
sections.add( sections.add(
@ -86,7 +85,6 @@ class RoomListSectionBuilderGroup(
) )
) )
} }
}
) )
} }
RoomListDisplayMode.NOTIFICATIONS -> { RoomListDisplayMode.NOTIFICATIONS -> {
@ -252,9 +250,7 @@ class RoomListSectionBuilderGroup(
@StringRes nameRes: Int, @StringRes nameRes: Int,
notifyOfLocalEcho: Boolean = false, notifyOfLocalEcho: Boolean = false,
query: (RoomSummaryQueryParams.Builder) -> Unit) { query: (RoomSummaryQueryParams.Builder) -> Unit) {
withQueryParams( withQueryParams(query) { roomQueryParams ->
{ query.invoke(it) },
{ roomQueryParams ->
val name = stringProvider.getString(nameRes) val name = stringProvider.getString(nameRes)
session.getFilteredPagedRoomSummariesLive(roomQueryParams) session.getFilteredPagedRoomSummariesLive(roomQueryParams)
.also { .also {
@ -276,13 +272,11 @@ class RoomListSectionBuilderGroup(
sectionName = name, sectionName = name,
livePages = livePagedList, livePages = livePagedList,
notifyOfLocalEcho = notifyOfLocalEcho, notifyOfLocalEcho = notifyOfLocalEcho,
itemCount = session.getRoomCountFlow(roomQueryParams) itemCount = session.getRoomCountLive(roomQueryParams).asFlow()
) )
) )
} }
} }
)
} }
private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) { private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) {

View file

@ -32,6 +32,7 @@ import im.vector.app.features.invite.showInvites
import im.vector.app.space import im.vector.app.space
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
@ -40,6 +41,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.ActiveSpaceFilter
import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter
@ -83,64 +85,10 @@ class RoomListSectionBuilderSpace(
} }
RoomListDisplayMode.FILTERED -> { RoomListDisplayMode.FILTERED -> {
// Used when searching for rooms // Used when searching for rooms
withQueryParams( buildFilteredSection(sections)
{
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.getRoomCountFlow(updatableFilterLivePageResult.queryParams) }
.distinctUntilChanged()
sections.add(
RoomsSection(
sectionName = name,
livePages = updatableFilterLivePageResult.livePagedList,
itemCount = itemCountFlow
)
)
}
}
)
} }
RoomListDisplayMode.NOTIFICATIONS -> { RoomListDisplayMode.NOTIFICATIONS -> {
if (autoAcceptInvites.showInvites()) { buildNotificationsSection(sections, activeSpaceAwareQueries)
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
}
} }
} }
@ -332,6 +280,68 @@ class RoomListSectionBuilderSpace(
} }
} }
private fun buildNotificationsSection(sections: MutableList<RoomsSection>,
activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
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<RoomsSection>) {
// 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<RoomsSection>, private fun addSection(sections: MutableList<RoomsSection>,
activeSpaceUpdaters: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>, activeSpaceUpdaters: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>,
@StringRes nameRes: Int, @StringRes nameRes: Int,
@ -339,21 +349,29 @@ class RoomListSectionBuilderSpace(
spaceFilterStrategy: RoomListViewModel.SpaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.NONE, spaceFilterStrategy: RoomListViewModel.SpaceFilterStrategy = RoomListViewModel.SpaceFilterStrategy.NONE,
countRoomAsNotif: Boolean = false, countRoomAsNotif: Boolean = false,
query: (RoomSummaryQueryParams.Builder) -> Unit) { query: (RoomSummaryQueryParams.Builder) -> Unit) {
withQueryParams( withQueryParams(query) { roomQueryParams ->
{ query.invoke(it) }, val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
{ roomQueryParams -> val liveQueryParams = MutableStateFlow(updatedQueryParams)
val itemCountFlow = liveQueryParams
.flatMapLatest {
session.getRoomCountLive(it).asFlow()
}
.flowOn(Dispatchers.Main)
.distinctUntilChanged()
val name = stringProvider.getString(nameRes) val name = stringProvider.getString(nameRes)
session.getFilteredPagedRoomSummariesLive( val filteredPagedRoomSummariesLive = session.getFilteredPagedRoomSummariesLive(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()), roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()),
pagedListConfig pagedListConfig
).also { )
when (spaceFilterStrategy) { when (spaceFilterStrategy) {
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> { RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> {
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater { activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
override fun updateForSpaceId(roomId: String?) { override fun updateForSpaceId(roomId: String?) {
it.queryParams = roomQueryParams.copy( filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
) )
liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams }
} }
}) })
} }
@ -361,14 +379,15 @@ class RoomListSectionBuilderSpace(
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater { activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
override fun updateForSpaceId(roomId: String?) { override fun updateForSpaceId(roomId: String?) {
if (roomId != null) { if (roomId != null) {
it.queryParams = roomQueryParams.copy( filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
) )
} else { } else {
it.queryParams = roomQueryParams.copy( filteredPagedRoomSummariesLive.queryParams = roomQueryParams.copy(
activeSpaceFilter = ActiveSpaceFilter.None activeSpaceFilter = ActiveSpaceFilter.None
) )
} }
liveQueryParams.update { filteredPagedRoomSummariesLive.queryParams }
} }
}) })
} }
@ -376,8 +395,8 @@ class RoomListSectionBuilderSpace(
// we ignore current space for this one // we ignore current space for this one
} }
} }
}.livePagedList
.let { livePagedList -> val livePagedList = filteredPagedRoomSummariesLive.livePagedList
// use it also as a source to update count // use it also as a source to update count
livePagedList.asFlow() livePagedList.asFlow()
.onEach { .onEach {
@ -397,13 +416,6 @@ class RoomListSectionBuilderSpace(
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.launchIn(viewModelScope) .launchIn(viewModelScope)
val itemCountFlow = livePagedList.asFlow()
.flatMapLatest {
val queryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
session.getRoomCountFlow(queryParams)
}
.distinctUntilChanged()
sections.add( sections.add(
RoomsSection( RoomsSection(
sectionName = name, sectionName = name,
@ -415,9 +427,6 @@ class RoomListSectionBuilderSpace(
} }
} }
)
}
private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) { private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) {
RoomSummaryQueryParams.Builder() RoomSummaryQueryParams.Builder()
.apply { builder.invoke(this) } .apply { builder.invoke(this) }

View file

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.list package im.vector.app.features.home.room.list
import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -28,6 +29,7 @@ import im.vector.app.databinding.ItemRoomCategoryBinding
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
class SectionHeaderAdapter constructor( class SectionHeaderAdapter constructor(
roomsSectionData: RoomsSectionData,
private val onClickAction: ClickListener private val onClickAction: ClickListener
) : RecyclerView.Adapter<SectionHeaderAdapter.VH>() { ) : RecyclerView.Adapter<SectionHeaderAdapter.VH>() {
@ -39,14 +41,16 @@ class SectionHeaderAdapter constructor(
val isHighlighted: Boolean = false, val isHighlighted: Boolean = false,
val isHidden: Boolean = true, val isHidden: Boolean = true,
// This will be false until real data has been submitted once // This will be false until real data has been submitted once
val isLoading: Boolean = true val isLoading: Boolean = true,
val isCollapsable: Boolean = false
) )
lateinit var roomsSectionData: RoomsSectionData var roomsSectionData: RoomsSectionData = roomsSectionData
private set private set
fun updateSection(newRoomsSectionData: RoomsSectionData) { fun updateSection(block: (RoomsSectionData) -> RoomsSectionData) {
if (!::roomsSectionData.isInitialized || newRoomsSectionData != roomsSectionData) { val newRoomsSectionData = block(roomsSectionData)
if (roomsSectionData != newRoomsSectionData) {
roomsSectionData = newRoomsSectionData roomsSectionData = newRoomsSectionData
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -82,11 +86,16 @@ class SectionHeaderAdapter constructor(
fun bind(roomsSectionData: RoomsSectionData) { fun bind(roomsSectionData: RoomsSectionData) {
binding.roomCategoryTitleView.text = roomsSectionData.name binding.roomCategoryTitleView.text = roomsSectionData.name
val tintColor = ThemeUtils.getColor(binding.root.context, R.attr.vctr_content_secondary) val tintColor = ThemeUtils.getColor(binding.root.context, R.attr.vctr_content_secondary)
val collapsableArrowDrawable: Drawable? = if (roomsSectionData.isCollapsable) {
val expandedArrowDrawableRes = if (roomsSectionData.isExpanded) R.drawable.ic_expand_more else R.drawable.ic_expand_less 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) DrawableCompat.setTint(it, tintColor)
} }
binding.roomCategoryCounterView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) } else {
null
}
binding.root.isClickable = roomsSectionData.isCollapsable
binding.roomCategoryCounterView.setCompoundDrawablesWithIntrinsicBounds(null, null, collapsableArrowDrawable, null)
binding.roomCategoryCounterView.text = roomsSectionData.itemCount.takeIf { it > 0 }?.toString().orEmpty() binding.roomCategoryCounterView.text = roomsSectionData.itemCount.takeIf { it > 0 }?.toString().orEmpty()
binding.roomCategoryUnreadCounterBadgeView.render(UnreadCounterBadgeView.State(roomsSectionData.notificationCount, roomsSectionData.isHighlighted)) binding.roomCategoryUnreadCounterBadgeView.render(UnreadCounterBadgeView.State(roomsSectionData.notificationCount, roomsSectionData.isHighlighted))
} }

View file

@ -75,9 +75,9 @@ import im.vector.app.features.onboarding.OnboardingActivity
import im.vector.app.features.pin.PinActivity import im.vector.app.features.pin.PinActivity
import im.vector.app.features.pin.PinArgs import im.vector.app.features.pin.PinArgs
import im.vector.app.features.pin.PinMode 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.CreatePollActivity
import im.vector.app.features.poll.create.CreatePollArgs import im.vector.app.features.poll.create.CreatePollArgs
import im.vector.app.features.poll.create.PollMode
import im.vector.app.features.roomdirectory.RoomDirectoryActivity import im.vector.app.features.roomdirectory.RoomDirectoryActivity
import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.RoomDirectoryData
import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity

View file

@ -31,7 +31,7 @@ import im.vector.app.features.location.LocationSharingMode
import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginConfig
import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.AttachmentData
import im.vector.app.features.pin.PinMode 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.RoomDirectoryData
import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData
import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.VectorSettingsActivity

View file

@ -524,7 +524,7 @@ class OnboardingViewModel @AssistedInject constructor(
onDirectLoginError(failure) onDirectLoginError(failure)
return return
} }
onSessionCreated(data, isAccountCreated = true) onSessionCreated(data, isAccountCreated = false)
} }
private fun onDirectLoginError(failure: Throwable) { private fun onDirectLoginError(failure: Throwable) {

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.poll.create package im.vector.app.features.poll
enum class PollMode { enum class PollMode {
CREATE, CREATE,

View file

@ -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 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
}

View file

@ -29,6 +29,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentCreatePollBinding 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 im.vector.app.features.poll.create.CreatePollViewModel.Companion.MAX_OPTIONS_COUNT
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.PollType

View file

@ -23,6 +23,7 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel 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.Session
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.PollType

View file

@ -17,6 +17,7 @@
package im.vector.app.features.poll.create package im.vector.app.features.poll.create
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import im.vector.app.features.poll.PollMode
import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.PollType
data class CreatePollViewState( data class CreatePollViewState(

View file

@ -84,7 +84,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
val spaceCountFlow: Flow<Int> by lazy { val spaceCountFlow: Flow<Int> by lazy {
spaceUpdatableLivePageResult.livePagedList.asFlow() spaceUpdatableLivePageResult.livePagedList.asFlow()
.flatMapLatest { session.getRoomCountFlow(spaceUpdatableLivePageResult.queryParams) } .flatMapLatest { session.getRoomCountLive(spaceUpdatableLivePageResult.queryParams).asFlow() }
.distinctUntilChanged() .distinctUntilChanged()
} }
@ -110,7 +110,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
val roomCountFlow: Flow<Int> by lazy { val roomCountFlow: Flow<Int> by lazy {
roomUpdatableLivePageResult.livePagedList.asFlow() roomUpdatableLivePageResult.livePagedList.asFlow()
.flatMapLatest { session.getRoomCountFlow(roomUpdatableLivePageResult.queryParams) } .flatMapLatest { session.getRoomCountLive(roomUpdatableLivePageResult.queryParams).asFlow() }
.distinctUntilChanged() .distinctUntilChanged()
} }
@ -136,7 +136,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
val dmCountFlow: Flow<Int> by lazy { val dmCountFlow: Flow<Int> by lazy {
dmUpdatableLivePageResult.livePagedList.asFlow() dmUpdatableLivePageResult.livePagedList.asFlow()
.flatMapLatest { session.getRoomCountFlow(dmUpdatableLivePageResult.queryParams) } .flatMapLatest { session.getRoomCountLive(dmUpdatableLivePageResult.queryParams).asFlow() }
.distinctUntilChanged() .distinctUntilChanged()
} }