mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 17:35:54 +03:00
Merge branch 'develop' into feature/aris/thread_live_thread_list
# Conflicts: # vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
This commit is contained in:
commit
8a862d006e
82 changed files with 1107 additions and 353 deletions
1
changelog.d/4533.misc
Normal file
1
changelog.d/4533.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Improve headers UI in Rooms/Messages lists
|
1
changelog.d/5340.bugfix
Normal file
1
changelog.d/5340.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Support both stable and unstable prefixes for Events about Polls and Location
|
1
changelog.d/5375.wip
Normal file
1
changelog.d/5375.wip
Normal file
|
@ -0,0 +1 @@
|
|||
Dynamically showing/hiding onboarding personalisation screens based on the users homeserver capabilities
|
1
changelog.d/5514.bugfix
Normal file
1
changelog.d/5514.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Read receipt in wrong order
|
|
@ -58,6 +58,7 @@ ext.libs = [
|
|||
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
|
||||
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
|
||||
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
|
||||
'lifecycleRuntimeKtx' : "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle",
|
||||
'datastore' : "androidx.datastore:datastore:1.0.0",
|
||||
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
|
||||
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
|
||||
|
@ -141,4 +142,4 @@ ext.libs = [
|
|||
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
|
||||
'junit' : "junit:junit:4.13.2"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
|
@ -353,7 +353,7 @@ fun Event.isAttachmentMessage(): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END
|
||||
fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END
|
||||
|
||||
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
|
||||
|
||||
|
@ -376,7 +376,7 @@ fun Event.getRelationContent(): RelationDefaultContent? {
|
|||
* Returns the poll question or null otherwise
|
||||
*/
|
||||
fun Event.getPollQuestion(): String? =
|
||||
getPollContent()?.pollCreationInfo?.question?.question
|
||||
getPollContent()?.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
|
||||
/**
|
||||
* Returns the relation content for a specific type or null otherwise
|
||||
|
|
|
@ -103,9 +103,9 @@ object EventType {
|
|||
const val REACTION = "m.reaction"
|
||||
|
||||
// Poll
|
||||
const val POLL_START = "org.matrix.msc3381.poll.start"
|
||||
const val POLL_RESPONSE = "org.matrix.msc3381.poll.response"
|
||||
const val POLL_END = "org.matrix.msc3381.poll.end"
|
||||
val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start")
|
||||
val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response")
|
||||
val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end")
|
||||
|
||||
// Unwedging
|
||||
internal const val DUMMY = "m.dummy"
|
||||
|
|
|
@ -18,6 +18,7 @@ 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
|
||||
|
@ -216,6 +217,11 @@ interface RoomService {
|
|||
pagedListConfig: PagedList.Config = defaultPagedListConfig,
|
||||
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
|
||||
|
||||
/**
|
||||
* Retrieve a flow on the number of rooms.
|
||||
*/
|
||||
fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int>
|
||||
|
||||
/**
|
||||
* TODO Doc
|
||||
*/
|
||||
|
|
|
@ -22,10 +22,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
|||
|
||||
interface UpdatableLivePageResult {
|
||||
val livePagedList: LiveData<PagedList<RoomSummary>>
|
||||
|
||||
fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams)
|
||||
|
||||
val liveBoundaries: LiveData<ResultBoundaries>
|
||||
var queryParams: RoomSummaryQueryParams
|
||||
}
|
||||
|
||||
data class ResultBoundaries(
|
||||
|
|
|
@ -39,37 +39,46 @@ data class MessageLocationContent(
|
|||
*/
|
||||
@Json(name = "geo_uri") val geoUri: String,
|
||||
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
/**
|
||||
* See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null,
|
||||
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
|
||||
@Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
|
||||
@Json(name = "m.location") val locationInfo: LocationInfo? = null,
|
||||
/**
|
||||
* Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3488.ts") val unstableTs: Long? = null,
|
||||
@Json(name = "m.ts") val ts: Long? = null,
|
||||
@Json(name = "org.matrix.msc1767.text") val unstableText: String? = null,
|
||||
@Json(name = "m.text") val text: String? = null,
|
||||
/**
|
||||
* m.asset defines a generic asset that can be used for location tracking but also in other places like
|
||||
* inventories, geofencing, checkins/checkouts etc.
|
||||
* It should contain a mandatory namespaced type key defining what particular asset is being referred to.
|
||||
* For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
|
||||
*/
|
||||
@Json(name = "m.asset") val locationAsset: LocationAsset? = null,
|
||||
|
||||
/**
|
||||
* Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3488.ts") val ts: Long? = null,
|
||||
|
||||
@Json(name = "org.matrix.msc1767.text") val text: String? = null
|
||||
@Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
|
||||
@Json(name = "m.asset") val locationAsset: LocationAsset? = null
|
||||
) : MessageContent {
|
||||
|
||||
fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
|
||||
fun getBestLocationInfo() = locationInfo ?: unstableLocationInfo
|
||||
|
||||
fun getBestTs() = ts ?: unstableTs
|
||||
|
||||
fun getBestText() = text ?: unstableText
|
||||
|
||||
fun getBestLocationAsset() = locationAsset ?: unstableLocationAsset
|
||||
|
||||
fun getBestGeoUri() = getBestLocationInfo()?.geoUri ?: geoUri
|
||||
|
||||
/**
|
||||
* @return true if the location asset is a user location, not a generic one.
|
||||
*/
|
||||
fun isSelfLocation(): Boolean {
|
||||
// Should behave like m.self if locationAsset is null
|
||||
val locationAsset = getBestLocationAsset()
|
||||
return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,5 +31,9 @@ data class MessagePollContent(
|
|||
@Json(name = "body") override val body: String = "",
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
|
||||
) : MessageContent
|
||||
@Json(name = "org.matrix.msc3381.poll.start") val unstablePollCreationInfo: PollCreationInfo? = null,
|
||||
@Json(name = "m.poll.start") val pollCreationInfo: PollCreationInfo? = null
|
||||
) : MessageContent {
|
||||
|
||||
fun getBestPollCreationInfo() = pollCreationInfo ?: unstablePollCreationInfo
|
||||
}
|
||||
|
|
|
@ -31,5 +31,9 @@ data class MessagePollResponseContent(
|
|||
@Json(name = "body") override val body: String = "",
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@Json(name = "org.matrix.msc3381.poll.response") val response: PollResponse? = null
|
||||
) : MessageContent
|
||||
@Json(name = "org.matrix.msc3381.poll.response") val unstableResponse: PollResponse? = null,
|
||||
@Json(name = "m.response") val response: PollResponse? = null
|
||||
) : MessageContent {
|
||||
|
||||
fun getBestResponse() = response ?: unstableResponse
|
||||
}
|
||||
|
|
|
@ -22,5 +22,9 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class PollAnswer(
|
||||
@Json(name = "id") val id: String? = null,
|
||||
@Json(name = "org.matrix.msc1767.text") val answer: String? = null
|
||||
)
|
||||
@Json(name = "org.matrix.msc1767.text") val unstableAnswer: String? = null,
|
||||
@Json(name = "m.text") val answer: String? = null
|
||||
) {
|
||||
|
||||
fun getBestAnswer() = answer ?: unstableAnswer
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollCreationInfo(
|
||||
@Json(name = "question") val question: PollQuestion? = null,
|
||||
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED,
|
||||
@Json(name = "max_selections") val maxSelections: Int = 1,
|
||||
@Json(name = "answers") val answers: List<PollAnswer>? = null
|
||||
@Json(name = "question") val question: PollQuestion? = null,
|
||||
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
|
||||
@Json(name = "max_selections") val maxSelections: Int = 1,
|
||||
@Json(name = "answers") val answers: List<PollAnswer>? = null
|
||||
)
|
||||
|
|
|
@ -21,5 +21,9 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollQuestion(
|
||||
@Json(name = "org.matrix.msc1767.text") val question: String? = null
|
||||
)
|
||||
@Json(name = "org.matrix.msc1767.text") val unstableQuestion: String? = null,
|
||||
@Json(name = "m.text") val question: String? = null
|
||||
) {
|
||||
|
||||
fun getBestQuestion() = question ?: unstableQuestion
|
||||
}
|
||||
|
|
|
@ -25,11 +25,17 @@ enum class PollType {
|
|||
* Voters should see results as soon as they have voted.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3381.poll.disclosed")
|
||||
DISCLOSED_UNSTABLE,
|
||||
|
||||
@Json(name = "m.poll.disclosed")
|
||||
DISCLOSED,
|
||||
|
||||
/**
|
||||
* Results should be only revealed when the poll is ended.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3381.poll.undisclosed")
|
||||
UNDISCLOSED_UNSTABLE,
|
||||
|
||||
@Json(name = "m.poll.undisclosed")
|
||||
UNDISCLOSED
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ object RoomSummaryConstants {
|
|||
EventType.CALL_ANSWER,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.STICKER,
|
||||
EventType.REACTION,
|
||||
EventType.POLL_START
|
||||
)
|
||||
EventType.REACTION
|
||||
) + EventType.POLL_START
|
||||
}
|
||||
|
|
|
@ -135,9 +135,9 @@ fun TimelineEvent.getEditedEventId(): String? {
|
|||
*/
|
||||
fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
||||
return when (root.getClearType()) {
|
||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
|||
|
||||
val allEvents = (newJoinEvents + inviteEvents).filter { event ->
|
||||
when (event.type) {
|
||||
EventType.POLL_START,
|
||||
in EventType.POLL_START,
|
||||
EventType.MESSAGE,
|
||||
EventType.REDACTION,
|
||||
EventType.ENCRYPTED,
|
||||
|
|
|
@ -20,6 +20,7 @@ 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
|
||||
|
@ -109,6 +110,10 @@ internal class DefaultRoomService @Inject constructor(
|
|||
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
|
||||
}
|
||||
|
||||
override fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int> {
|
||||
return roomSummaryDataSource.getCountFlow(queryParams)
|
||||
}
|
||||
|
||||
override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
|
||||
return roomSummaryDataSource.getNotificationCountForRooms(queryParams)
|
||||
}
|
||||
|
|
|
@ -87,11 +87,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
// TODO Add ?
|
||||
// EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.POLL_START,
|
||||
EventType.POLL_RESPONSE,
|
||||
EventType.POLL_END
|
||||
)
|
||||
EventType.ENCRYPTED
|
||||
) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END
|
||||
|
||||
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||
return allowedTypes.contains(eventType)
|
||||
|
@ -157,7 +154,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
} else if (event.getClearType() == EventType.POLL_RESPONSE) {
|
||||
} else if (event.getClearType() in EventType.POLL_RESPONSE) {
|
||||
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent ->
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
|
@ -178,12 +175,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
handleVerification(realm, event, roomId, isLocalEcho, it)
|
||||
}
|
||||
}
|
||||
EventType.POLL_RESPONSE -> {
|
||||
in EventType.POLL_RESPONSE -> {
|
||||
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
|
||||
handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId)
|
||||
}
|
||||
}
|
||||
EventType.POLL_END -> {
|
||||
in EventType.POLL_END -> {
|
||||
event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
|
||||
handleEndPoll(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
|
@ -228,7 +225,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
EventType.POLL_START -> {
|
||||
in EventType.POLL_START -> {
|
||||
val content: MessagePollContent? = event.content.toModel()
|
||||
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
|
@ -236,12 +233,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
handleReplace(realm, event, content, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
EventType.POLL_RESPONSE -> {
|
||||
in EventType.POLL_RESPONSE -> {
|
||||
event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
|
||||
handleResponse(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
EventType.POLL_END -> {
|
||||
in EventType.POLL_END -> {
|
||||
event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
|
||||
handleEndPoll(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
|
@ -423,12 +420,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
val option = content.response?.answers?.first() ?: return Unit.also {
|
||||
val option = content.getBestResponse()?.answers?.first() ?: return Unit.also {
|
||||
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
|
||||
}
|
||||
|
||||
// Check if this option is in available options
|
||||
if (!targetPollContent.pollCreationInfo?.answers?.map { it.id }?.contains(option).orFalse()) {
|
||||
if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(option).orFalse()) {
|
||||
Timber.v("## POLL $targetEventId doesn't contain option $option")
|
||||
return
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
|
|||
when (typeToPrune) {
|
||||
EventType.ENCRYPTED,
|
||||
EventType.MESSAGE,
|
||||
EventType.POLL_START -> {
|
||||
in EventType.POLL_START -> {
|
||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||
?: UnsignedData(null, null)
|
||||
|
|
|
@ -137,16 +137,11 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
options: List<String>,
|
||||
pollType: PollType): MessagePollContent {
|
||||
return MessagePollContent(
|
||||
pollCreationInfo = PollCreationInfo(
|
||||
question = PollQuestion(
|
||||
question = question
|
||||
),
|
||||
unstablePollCreationInfo = PollCreationInfo(
|
||||
question = PollQuestion(unstableQuestion = question),
|
||||
kind = pollType,
|
||||
answers = options.map { option ->
|
||||
PollAnswer(
|
||||
id = UUID.randomUUID().toString(),
|
||||
answer = option
|
||||
)
|
||||
PollAnswer(id = UUID.randomUUID().toString(), unstableAnswer = option)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -167,7 +162,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_START,
|
||||
type = EventType.POLL_START.first(),
|
||||
content = newContent.toContent()
|
||||
)
|
||||
}
|
||||
|
@ -179,11 +174,9 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
body = answerId,
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.REFERENCE,
|
||||
eventId = pollEventId),
|
||||
response = PollResponse(
|
||||
answers = listOf(answerId)
|
||||
)
|
||||
|
||||
eventId = pollEventId
|
||||
),
|
||||
unstableResponse = PollResponse(answers = listOf(answerId))
|
||||
)
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
return Event(
|
||||
|
@ -191,7 +184,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_RESPONSE,
|
||||
type = EventType.POLL_RESPONSE.first(),
|
||||
content = content.toContent(),
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||
}
|
||||
|
@ -207,7 +200,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_START,
|
||||
type = EventType.POLL_START.first(),
|
||||
content = content.toContent(),
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||
}
|
||||
|
@ -226,7 +219,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_END,
|
||||
type = EventType.POLL_END.first(),
|
||||
content = content.toContent(),
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||
}
|
||||
|
@ -239,15 +232,10 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
val content = MessageLocationContent(
|
||||
geoUri = geoUri,
|
||||
body = geoUri,
|
||||
locationInfo = LocationInfo(
|
||||
geoUri = geoUri,
|
||||
description = geoUri
|
||||
),
|
||||
locationAsset = LocationAsset(
|
||||
type = LocationAssetType.SELF
|
||||
),
|
||||
ts = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
|
||||
text = geoUri
|
||||
unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
|
||||
unstableLocationAsset = LocationAsset(type = LocationAssetType.SELF),
|
||||
unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
|
||||
unstableText = geoUri
|
||||
)
|
||||
return createMessageEvent(roomId, content)
|
||||
}
|
||||
|
@ -644,7 +632,9 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
|
||||
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
|
||||
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
|
||||
MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.pollCreationInfo?.question?.question ?: "")
|
||||
MessageType.MSGTYPE_POLL_START -> {
|
||||
return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
|
||||
}
|
||||
else -> return TextContent(content?.body ?: "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,13 @@ 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
|
||||
import org.matrix.android.sdk.api.query.isNormalized
|
||||
|
@ -42,6 +48,7 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification
|
|||
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||
import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||
|
@ -55,8 +62,10 @@ import javax.inject.Inject
|
|||
|
||||
internal class RoomSummaryDataSource @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val realmSessionProvider: RealmSessionProvider,
|
||||
private val roomSummaryMapper: RoomSummaryMapper,
|
||||
private val queryStringValueProcessor: QueryStringValueProcessor
|
||||
private val queryStringValueProcessor: QueryStringValueProcessor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) {
|
||||
|
||||
fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
|
||||
|
@ -219,17 +228,29 @@ internal class RoomSummaryDataSource @Inject constructor(
|
|||
return object : UpdatableLivePageResult {
|
||||
override val livePagedList: LiveData<PagedList<RoomSummary>> = mapped
|
||||
|
||||
override fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) {
|
||||
realmDataSourceFactory.updateQuery {
|
||||
roomSummariesQuery(it, builder.invoke(queryParams)).process(sortOrder)
|
||||
}
|
||||
}
|
||||
|
||||
override val liveBoundaries: LiveData<ResultBoundaries>
|
||||
get() = boundaries
|
||||
|
||||
override var queryParams: RoomSummaryQueryParams = queryParams
|
||||
set(value) {
|
||||
field = value
|
||||
realmDataSourceFactory.updateQuery {
|
||||
roomSummariesQuery(it, value).process(sortOrder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int> =
|
||||
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 getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
|
||||
var notificationCount: RoomAggregateNotificationCount? = null
|
||||
monarchy.doWithRealm { realm ->
|
||||
|
|
|
@ -355,6 +355,7 @@ dependencies {
|
|||
// Lifecycle
|
||||
implementation libs.androidx.lifecycleLivedata
|
||||
implementation libs.androidx.lifecycleProcess
|
||||
implementation libs.androidx.lifecycleRuntimeKtx
|
||||
|
||||
implementation libs.androidx.datastore
|
||||
implementation libs.androidx.datastorepreferences
|
||||
|
|
|
@ -22,13 +22,17 @@ import androidx.datastore.preferences.core.Preferences
|
|||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import im.vector.app.features.HomeserverCapabilitiesOverride
|
||||
import im.vector.app.features.VectorOverrides
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vector_overrides")
|
||||
private val keyForceDialPadDisplay = booleanPreferencesKey("force_dial_pad_display")
|
||||
private val keyForceLoginFallback = booleanPreferencesKey("force_login_fallback")
|
||||
private val forceCanChangeDisplayName = booleanPreferencesKey("force_can_change_display_name")
|
||||
private val forceCanChangeAvatar = booleanPreferencesKey("force_can_change_avatar")
|
||||
|
||||
class DebugVectorOverrides(private val context: Context) : VectorOverrides {
|
||||
|
||||
|
@ -40,6 +44,13 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides {
|
|||
preferences[keyForceLoginFallback].orFalse()
|
||||
}
|
||||
|
||||
override val forceHomeserverCapabilities = context.dataStore.data.map { preferences ->
|
||||
HomeserverCapabilitiesOverride(
|
||||
canChangeDisplayName = preferences[forceCanChangeDisplayName],
|
||||
canChangeAvatar = preferences[forceCanChangeAvatar]
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setForceDialPadDisplay(force: Boolean) {
|
||||
context.dataStore.edit { settings ->
|
||||
settings[keyForceDialPadDisplay] = force
|
||||
|
@ -51,4 +62,18 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides {
|
|||
settings[keyForceLoginFallback] = force
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setHomeserverCapabilities(block: HomeserverCapabilitiesOverride.() -> HomeserverCapabilitiesOverride) {
|
||||
val capabilitiesOverride = block(forceHomeserverCapabilities.firstOrNull() ?: HomeserverCapabilitiesOverride(null, null))
|
||||
context.dataStore.edit { settings ->
|
||||
when (capabilitiesOverride.canChangeDisplayName) {
|
||||
null -> settings.remove(forceCanChangeDisplayName)
|
||||
else -> settings[forceCanChangeDisplayName] = capabilitiesOverride.canChangeDisplayName
|
||||
}
|
||||
when (capabilitiesOverride.canChangeAvatar) {
|
||||
null -> settings.remove(forceCanChangeAvatar)
|
||||
else -> settings[forceCanChangeAvatar] = capabilitiesOverride.canChangeAvatar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,12 @@ class DebugPrivateSettingsFragment : VectorBaseFragment<FragmentDebugPrivateSett
|
|||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
views.forceDialPadTabDisplay.isChecked = it.dialPadVisible
|
||||
views.forceChangeDisplayNameCapability.bind(it.homeserverCapabilityOverrides.displayName) { option ->
|
||||
viewModel.handle(DebugPrivateSettingsViewActions.SetDisplayNameCapabilityOverride(option))
|
||||
}
|
||||
views.forceChangeAvatarCapability.bind(it.homeserverCapabilityOverrides.avatar) { option ->
|
||||
viewModel.handle(DebugPrivateSettingsViewActions.SetAvatarCapabilityOverride(option))
|
||||
}
|
||||
views.forceLoginFallback.isChecked = it.forceLoginFallback
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ package im.vector.app.features.debug.settings
|
|||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class DebugPrivateSettingsViewActions : VectorViewModelAction {
|
||||
data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions()
|
||||
data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions()
|
||||
sealed interface DebugPrivateSettingsViewActions : VectorViewModelAction {
|
||||
data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions
|
||||
data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions
|
||||
data class SetDisplayNameCapabilityOverride(val option: BooleanHomeserverCapabilitiesOverride?) : DebugPrivateSettingsViewActions
|
||||
data class SetAvatarCapabilityOverride(val option: BooleanHomeserverCapabilitiesOverride?) : DebugPrivateSettingsViewActions
|
||||
}
|
||||
|
|
|
@ -22,9 +22,12 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.debug.features.DebugVectorOverrides
|
||||
import im.vector.app.features.debug.settings.DebugPrivateSettingsViewActions.SetAvatarCapabilityOverride
|
||||
import im.vector.app.features.debug.settings.DebugPrivateSettingsViewActions.SetDisplayNameCapabilityOverride
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DebugPrivateSettingsViewModel @AssistedInject constructor(
|
||||
|
@ -40,10 +43,10 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
|
|||
companion object : MavericksViewModelFactory<DebugPrivateSettingsViewModel, DebugPrivateSettingsViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
init {
|
||||
observeVectorDataStore()
|
||||
observeVectorOverrides()
|
||||
}
|
||||
|
||||
private fun observeVectorDataStore() {
|
||||
private fun observeVectorOverrides() {
|
||||
debugVectorOverrides.forceDialPad.setOnEach {
|
||||
copy(
|
||||
dialPadVisible = it
|
||||
|
@ -52,13 +55,23 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
|
|||
debugVectorOverrides.forceLoginFallback.setOnEach {
|
||||
copy(forceLoginFallback = it)
|
||||
}
|
||||
debugVectorOverrides.forceHomeserverCapabilities.setOnEach {
|
||||
val activeDisplayNameOption = BooleanHomeserverCapabilitiesOverride.from(it.canChangeDisplayName)
|
||||
val activeAvatarOption = BooleanHomeserverCapabilitiesOverride.from(it.canChangeAvatar)
|
||||
copy(homeserverCapabilityOverrides = homeserverCapabilityOverrides.copy(
|
||||
displayName = homeserverCapabilityOverrides.displayName.copy(activeOption = activeDisplayNameOption),
|
||||
avatar = homeserverCapabilityOverrides.avatar.copy(activeOption = activeAvatarOption),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: DebugPrivateSettingsViewActions) {
|
||||
when (action) {
|
||||
is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action)
|
||||
is DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled -> handleSetForceLoginFallbackEnabled(action)
|
||||
}
|
||||
is SetDisplayNameCapabilityOverride -> handSetDisplayNameCapabilityOverride(action)
|
||||
is SetAvatarCapabilityOverride -> handSetAvatarCapabilityOverride(action)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleSetDialPadVisibility(action: DebugPrivateSettingsViewActions.SetDialPadVisibility) {
|
||||
|
@ -72,4 +85,18 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor(
|
|||
debugVectorOverrides.setForceLoginFallback(action.force)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handSetDisplayNameCapabilityOverride(action: SetDisplayNameCapabilityOverride) {
|
||||
viewModelScope.launch {
|
||||
val forceDisplayName = action.option.toBoolean()
|
||||
debugVectorOverrides.setHomeserverCapabilities { copy(canChangeDisplayName = forceDisplayName) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handSetAvatarCapabilityOverride(action: SetAvatarCapabilityOverride) {
|
||||
viewModelScope.launch {
|
||||
val forceAvatar = action.option.toBoolean()
|
||||
debugVectorOverrides.setHomeserverCapabilities { copy(canChangeAvatar = forceAvatar) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,23 @@
|
|||
package im.vector.app.features.debug.settings
|
||||
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import im.vector.app.features.debug.settings.OverrideDropdownView.OverrideDropdown
|
||||
|
||||
data class DebugPrivateSettingsViewState(
|
||||
val dialPadVisible: Boolean = false,
|
||||
val forceLoginFallback: Boolean = false,
|
||||
val homeserverCapabilityOverrides: HomeserverCapabilityOverrides = HomeserverCapabilityOverrides()
|
||||
) : MavericksState
|
||||
|
||||
data class HomeserverCapabilityOverrides(
|
||||
val displayName: OverrideDropdown<BooleanHomeserverCapabilitiesOverride> = OverrideDropdown(
|
||||
label = "Override display name capability",
|
||||
activeOption = null,
|
||||
options = listOf(BooleanHomeserverCapabilitiesOverride.ForceEnabled, BooleanHomeserverCapabilitiesOverride.ForceDisabled)
|
||||
),
|
||||
val avatar: OverrideDropdown<BooleanHomeserverCapabilitiesOverride> = OverrideDropdown(
|
||||
label = "Override avatar capability",
|
||||
activeOption = null,
|
||||
options = listOf(BooleanHomeserverCapabilitiesOverride.ForceEnabled, BooleanHomeserverCapabilitiesOverride.ForceDisabled)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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.debug.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import im.vector.app.R
|
||||
import im.vector.app.databinding.ViewBooleanDropdownBinding
|
||||
|
||||
class OverrideDropdownView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs) {
|
||||
|
||||
private val binding = ViewBooleanDropdownBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
this
|
||||
)
|
||||
|
||||
init {
|
||||
orientation = HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
}
|
||||
|
||||
fun <T : OverrideOption> bind(feature: OverrideDropdown<T>, listener: Listener<T>) {
|
||||
binding.overrideLabel.text = feature.label
|
||||
|
||||
binding.overrideOptions.apply {
|
||||
val arrayAdapter = ArrayAdapter<String>(context, android.R.layout.simple_spinner_dropdown_item)
|
||||
val options = listOf("Inactive") + feature.options.map { it.label }
|
||||
arrayAdapter.addAll(options)
|
||||
adapter = arrayAdapter
|
||||
|
||||
feature.activeOption?.let {
|
||||
setSelection(options.indexOf(it.label), false)
|
||||
}
|
||||
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
when (position) {
|
||||
0 -> listener.onOverrideSelected(option = null)
|
||||
else -> listener.onOverrideSelected(feature.options[position - 1])
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun interface Listener<T> {
|
||||
fun onOverrideSelected(option: T?)
|
||||
}
|
||||
|
||||
data class OverrideDropdown<T : OverrideOption>(
|
||||
val label: String,
|
||||
val options: List<T>,
|
||||
val activeOption: T?,
|
||||
)
|
||||
}
|
||||
|
||||
interface OverrideOption {
|
||||
val label: String
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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.debug.settings
|
||||
|
||||
sealed interface BooleanHomeserverCapabilitiesOverride : OverrideOption {
|
||||
|
||||
companion object {
|
||||
fun from(value: Boolean?) = when (value) {
|
||||
null -> null
|
||||
true -> ForceEnabled
|
||||
false -> ForceDisabled
|
||||
}
|
||||
}
|
||||
|
||||
object ForceEnabled : BooleanHomeserverCapabilitiesOverride {
|
||||
override val label = "Force enabled"
|
||||
}
|
||||
|
||||
object ForceDisabled : BooleanHomeserverCapabilitiesOverride {
|
||||
override val label = "Force disabled"
|
||||
}
|
||||
}
|
||||
|
||||
fun BooleanHomeserverCapabilitiesOverride?.toBoolean() = when (this) {
|
||||
null -> null
|
||||
BooleanHomeserverCapabilitiesOverride.ForceDisabled -> false
|
||||
BooleanHomeserverCapabilitiesOverride.ForceEnabled -> true
|
||||
}
|
|
@ -31,6 +31,24 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:text="Force login and registration fallback" />
|
||||
|
||||
<im.vector.app.features.debug.settings.OverrideDropdownView
|
||||
android:id="@+id/forceChangeDisplayNameCapability"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<im.vector.app.features.debug.settings.OverrideDropdownView
|
||||
android:id="@+id/forceChangeAvatarCapability"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
|
25
vector/src/debug/res/layout/view_boolean_dropdown.xml
Normal file
25
vector/src/debug/res/layout/view_boolean_dropdown.xml
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:parentTag="android.widget.LinearLayout">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/overrideLabel"
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:textColor="?vctr_content_primary"
|
||||
tools:text="Login version" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatSpinner
|
||||
android:id="@+id/overrideOptions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</merge>
|
|
@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|||
|
||||
fun TimelineEvent.canReact(): Boolean {
|
||||
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
|
||||
return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER, EventType.POLL_START) &&
|
||||
return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START &&
|
||||
root.sendState == SendState.SYNCED &&
|
||||
!root.isRedacted()
|
||||
}
|
||||
|
|
|
@ -22,9 +22,16 @@ import kotlinx.coroutines.flow.flowOf
|
|||
interface VectorOverrides {
|
||||
val forceDialPad: Flow<Boolean>
|
||||
val forceLoginFallback: Flow<Boolean>
|
||||
val forceHomeserverCapabilities: Flow<HomeserverCapabilitiesOverride>?
|
||||
}
|
||||
|
||||
data class HomeserverCapabilitiesOverride(
|
||||
val canChangeDisplayName: Boolean?,
|
||||
val canChangeAvatar: Boolean?
|
||||
)
|
||||
|
||||
class DefaultVectorOverrides : VectorOverrides {
|
||||
override val forceDialPad = flowOf(false)
|
||||
override val forceLoginFallback = flowOf(false)
|
||||
override val forceHomeserverCapabilities: Flow<HomeserverCapabilitiesOverride>? = null
|
||||
}
|
||||
|
|
|
@ -1189,7 +1189,7 @@ class TimelineFragment @Inject constructor(
|
|||
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
|
||||
getString(R.string.voice_message_reply_content, formattedDuration)
|
||||
} else if (messageContent is MessagePollContent) {
|
||||
messageContent.pollCreationInfo?.question?.question
|
||||
messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
} else {
|
||||
messageContent?.body ?: ""
|
||||
}
|
||||
|
@ -2165,7 +2165,7 @@ class TimelineFragment @Inject constructor(
|
|||
timelineViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
|
||||
}
|
||||
is EventSharedAction.Edit -> {
|
||||
if (action.eventType == EventType.POLL_START) {
|
||||
if (action.eventType in EventType.POLL_START) {
|
||||
navigator.openCreatePoll(requireContext(), timelineArgs.roomId, action.eventId, PollMode.EDIT)
|
||||
} else if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
|
||||
messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString()))
|
||||
|
|
|
@ -181,7 +181,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
} else {
|
||||
when (timelineEvent.root.getClearType()) {
|
||||
EventType.MESSAGE,
|
||||
EventType.STICKER -> {
|
||||
EventType.STICKER -> {
|
||||
val messageContent: MessageContent? = timelineEvent.getLastMessageContent()
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
|
||||
val html = messageContent.formattedBody
|
||||
|
@ -207,13 +207,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
EventType.CALL_INVITE,
|
||||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> {
|
||||
EventType.CALL_ANSWER -> {
|
||||
noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
|
||||
}
|
||||
EventType.POLL_START -> {
|
||||
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.pollCreationInfo?.question?.question ?: ""
|
||||
in EventType.POLL_START -> {
|
||||
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)
|
||||
?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
|
||||
}
|
||||
else -> null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
|
@ -373,7 +374,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
|
||||
if (canRedact(timelineEvent, actionPermissions)) {
|
||||
if (timelineEvent.root.getClearType() == EventType.POLL_START) {
|
||||
if (timelineEvent.root.getClearType() in EventType.POLL_START) {
|
||||
add(EventSharedAction.Redact(
|
||||
eventId,
|
||||
askForReason = informationData.senderId != session.myUserId,
|
||||
|
@ -425,7 +426,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
|
||||
private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
|
||||
// Only EventType.MESSAGE and EventType.POLL_START event types are supported for the moment
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false
|
||||
if (event.root.getClearType() !in EventType.POLL_START + EventType.MESSAGE) return false
|
||||
if (!actionPermissions.canSendMessage) return false
|
||||
return when (messageContent?.msgType) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
|
@ -511,7 +512,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
|
||||
private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
|
||||
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.POLL_START)) return false
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false
|
||||
// Message sent by the current user can always be redacted
|
||||
if (event.root.senderId == session.myUserId) return true
|
||||
// Check permission for messages sent by other users
|
||||
|
@ -526,13 +527,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
|
||||
private fun canViewReactions(event: TimelineEvent): Boolean {
|
||||
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.POLL_START)) return false
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false
|
||||
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
private fun canEdit(event: TimelineEvent, myUserId: String, actionPermissions: ActionPermissions): Boolean {
|
||||
// Only event of type EventType.MESSAGE and EventType.POLL_START are supported for the moment
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false
|
||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE) + EventType.POLL_START) return false
|
||||
if (!actionPermissions.canSendMessage) return false
|
||||
// TODO if user is admin or moderator
|
||||
val messageContent = event.root.getClearContent().toModel<MessageContent>()
|
||||
|
@ -578,13 +579,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
|
||||
private fun canEndPoll(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
|
||||
return event.root.getClearType() == EventType.POLL_START &&
|
||||
return event.root.getClearType() in EventType.POLL_START &&
|
||||
canRedact(event, actionPermissions) &&
|
||||
event.annotations?.pollResponseSummary?.closedTime == null
|
||||
}
|
||||
|
||||
private fun canEditPoll(event: TimelineEvent): Boolean {
|
||||
return event.root.getClearType() == EventType.POLL_START &&
|
||||
return event.root.getClearType() in EventType.POLL_START &&
|
||||
event.annotations?.pollResponseSummary?.closedTime == null &&
|
||||
event.annotations?.pollResponseSummary?.aggregatedContent?.totalVotes ?: 0 == 0
|
||||
}
|
||||
|
|
|
@ -247,7 +247,7 @@ class MessageItemFactory @Inject constructor(
|
|||
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
|
||||
val winnerVoteCount = pollResponseSummary?.winnerVoteCount
|
||||
val isPollSent = informationData.sendState.isSent()
|
||||
val isPollUndisclosed = pollContent.pollCreationInfo?.kind == PollType.UNDISCLOSED
|
||||
val isPollUndisclosed = pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED_UNSTABLE
|
||||
|
||||
val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let {
|
||||
when {
|
||||
|
@ -262,13 +262,13 @@ class MessageItemFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
pollContent.pollCreationInfo?.answers?.forEach { option ->
|
||||
pollContent.getBestPollCreationInfo()?.answers?.forEach { option ->
|
||||
val voteSummary = pollResponseSummary?.votes?.get(option.id)
|
||||
val isMyVote = pollResponseSummary?.myVote == option.id
|
||||
val voteCount = voteSummary?.total ?: 0
|
||||
val votePercentage = voteSummary?.percentage ?: 0.0
|
||||
val optionId = option.id ?: ""
|
||||
val optionAnswer = option.answer ?: ""
|
||||
val optionAnswer = option.getBestAnswer() ?: ""
|
||||
|
||||
optionViewStates.add(
|
||||
if (!isPollSent) {
|
||||
|
@ -291,7 +291,7 @@ class MessageItemFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
val question = pollContent.pollCreationInfo?.question?.question ?: ""
|
||||
val question = pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() ?: ""
|
||||
|
||||
return PollItem_()
|
||||
.attributes(attributes)
|
||||
|
|
|
@ -38,8 +38,7 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
|
|||
.map {
|
||||
ReadReceiptData(it.roomMember.userId, it.roomMember.avatarUrl, it.roomMember.displayName, it.originServerTs)
|
||||
}
|
||||
.toList()
|
||||
|
||||
.sortedByDescending { it.timestamp }
|
||||
return ReadReceiptsItem_()
|
||||
.id("read_receipts_$eventId")
|
||||
.eventId(eventId)
|
||||
|
|
|
@ -94,7 +94,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
when (event.root.getClearType()) {
|
||||
// Message itemsX
|
||||
EventType.STICKER,
|
||||
EventType.POLL_START,
|
||||
in EventType.POLL_START,
|
||||
EventType.MESSAGE -> messageItemFactory.create(params)
|
||||
EventType.REDACTION,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
|
@ -107,8 +107,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
EventType.CALL_SELECT_ANSWER,
|
||||
EventType.CALL_NEGOTIATE,
|
||||
EventType.REACTION,
|
||||
EventType.POLL_RESPONSE,
|
||||
EventType.POLL_END -> noticeItemFactory.create(params)
|
||||
in EventType.POLL_RESPONSE,
|
||||
in EventType.POLL_END -> noticeItemFactory.create(params)
|
||||
// Calls
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
|
|
|
@ -122,14 +122,14 @@ class DisplayableEventFormatter @Inject constructor(
|
|||
EventType.CALL_CANDIDATES -> {
|
||||
span { }
|
||||
}
|
||||
EventType.POLL_START -> {
|
||||
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.pollCreationInfo?.question?.question
|
||||
in EventType.POLL_START -> {
|
||||
timelineEvent.root.getClearContent().toModel<MessagePollContent>(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
?: stringProvider.getString(R.string.sent_a_poll)
|
||||
}
|
||||
EventType.POLL_RESPONSE -> {
|
||||
in EventType.POLL_RESPONSE -> {
|
||||
stringProvider.getString(R.string.poll_response_room_list_preview)
|
||||
}
|
||||
EventType.POLL_END -> {
|
||||
in EventType.POLL_END -> {
|
||||
stringProvider.getString(R.string.poll_end_room_list_preview)
|
||||
}
|
||||
else -> {
|
||||
|
|
|
@ -106,8 +106,8 @@ class NoticeEventFormatter @Inject constructor(
|
|||
EventType.STATE_SPACE_PARENT,
|
||||
EventType.REDACTION,
|
||||
EventType.STICKER,
|
||||
EventType.POLL_RESPONSE,
|
||||
EventType.POLL_END -> formatDebug(timelineEvent.root)
|
||||
in EventType.POLL_RESPONSE,
|
||||
in EventType.POLL_END -> formatDebug(timelineEvent.root)
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
null
|
||||
|
@ -196,8 +196,8 @@ class NoticeEventFormatter @Inject constructor(
|
|||
}
|
||||
|
||||
private fun formatDebug(event: Event): CharSequence {
|
||||
val threadPrefix = if (event.isThread()) "thread" else ""
|
||||
return "Debug: $threadPrefix event type \"${event.getClearType()}\""
|
||||
val threadPrefix = if (event.isThread()) "thread" else ""
|
||||
return "Debug: $threadPrefix event type \"${event.getClearType()}\""
|
||||
}
|
||||
|
||||
private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? {
|
||||
|
|
|
@ -50,9 +50,8 @@ object TimelineDisplayableEvents {
|
|||
EventType.STATE_ROOM_TOMBSTONE,
|
||||
EventType.STATE_ROOM_JOIN_RULES,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.POLL_START
|
||||
)
|
||||
EventType.KEY_VERIFICATION_CANCEL
|
||||
) + EventType.POLL_START
|
||||
}
|
||||
|
||||
fun TimelineEvent.canBeMerged(): Boolean {
|
||||
|
|
|
@ -106,8 +106,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||
holder.timeView.isVisible = false
|
||||
}
|
||||
|
||||
holder.additionalTopSpace.isVisible = attributes.informationData.messageLayout.addTopMargin
|
||||
|
||||
// Render send state indicator
|
||||
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
||||
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
||||
|
@ -157,7 +155,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||
|
||||
abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) {
|
||||
|
||||
val additionalTopSpace by bind<View>(R.id.additionalTopSpace)
|
||||
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
|
|
|
@ -21,43 +21,44 @@ import im.vector.app.R
|
|||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
sealed interface TimelineMessageLayout : Parcelable {
|
||||
|
||||
val layoutRes: Int
|
||||
val showAvatar: Boolean
|
||||
val showDisplayName: Boolean
|
||||
val addTopMargin: Boolean
|
||||
val showTimestamp: Boolean
|
||||
|
||||
@Parcelize
|
||||
data class Default(override val showAvatar: Boolean,
|
||||
override val showDisplayName: Boolean,
|
||||
override val showTimestamp: Boolean,
|
||||
override val addTopMargin: Boolean = false,
|
||||
// Keep defaultLayout generated on epoxy items
|
||||
override val layoutRes: Int = 0) : TimelineMessageLayout
|
||||
data class Default(
|
||||
override val showAvatar: Boolean,
|
||||
override val showDisplayName: Boolean,
|
||||
override val showTimestamp: Boolean,
|
||||
// Keep defaultLayout generated on epoxy items
|
||||
override val layoutRes: Int = 0,
|
||||
) : TimelineMessageLayout
|
||||
|
||||
@Parcelize
|
||||
data class Bubble(
|
||||
override val showAvatar: Boolean,
|
||||
override val showDisplayName: Boolean,
|
||||
override val showTimestamp: Boolean = true,
|
||||
override val addTopMargin: Boolean = false,
|
||||
val isIncoming: Boolean,
|
||||
val isPseudoBubble: Boolean,
|
||||
val cornersRadius: CornersRadius,
|
||||
val timestampAsOverlay: Boolean,
|
||||
override val layoutRes: Int = if (isIncoming) {
|
||||
R.layout.item_timeline_event_bubble_incoming_base
|
||||
} else {
|
||||
R.layout.item_timeline_event_bubble_outgoing_base
|
||||
}
|
||||
override val showAvatar: Boolean,
|
||||
override val showDisplayName: Boolean,
|
||||
override val showTimestamp: Boolean = true,
|
||||
val addTopMargin: Boolean = false,
|
||||
val isIncoming: Boolean,
|
||||
val isPseudoBubble: Boolean,
|
||||
val cornersRadius: CornersRadius,
|
||||
val timestampAsOverlay: Boolean,
|
||||
override val layoutRes: Int = if (isIncoming) {
|
||||
R.layout.item_timeline_event_bubble_incoming_base
|
||||
} else {
|
||||
R.layout.item_timeline_event_bubble_outgoing_base
|
||||
},
|
||||
) : TimelineMessageLayout {
|
||||
|
||||
@Parcelize
|
||||
data class CornersRadius(
|
||||
val topStartRadius: Float,
|
||||
val topEndRadius: Float,
|
||||
val bottomStartRadius: Float,
|
||||
val bottomEndRadius: Float
|
||||
val topStartRadius: Float,
|
||||
val topEndRadius: Float,
|
||||
val bottomStartRadius: Float,
|
||||
val bottomEndRadius: Float,
|
||||
) : Parcelable
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,10 +43,9 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
|||
// Can be rendered in bubbles, other types will fallback to default
|
||||
private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf(
|
||||
EventType.MESSAGE,
|
||||
EventType.POLL_START,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.STICKER
|
||||
)
|
||||
) + EventType.POLL_START
|
||||
|
||||
// Can't be rendered in bubbles, so get back to default layout
|
||||
private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf(
|
||||
|
|
|
@ -42,9 +42,11 @@ import im.vector.app.features.home.room.detail.timeline.style.shapeAppearanceMod
|
|||
import im.vector.app.features.themes.ThemeUtils
|
||||
import timber.log.Timber
|
||||
|
||||
class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0) :
|
||||
RelativeLayout(context, attrs, defStyleAttr), TimelineMessageLayoutRenderer {
|
||||
class MessageBubbleView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : RelativeLayout(context, attrs, defStyleAttr), TimelineMessageLayoutRenderer {
|
||||
|
||||
private var isIncoming: Boolean = false
|
||||
|
||||
|
@ -87,22 +89,43 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
|
|||
outlineProvider = ViewOutlineProvider.BACKGROUND
|
||||
clipToOutline = true
|
||||
background = RippleDrawable(
|
||||
ContextCompat.getColorStateList(context, R.color.mtrl_btn_ripple_color) ?: ColorStateList.valueOf(Color.TRANSPARENT),
|
||||
bubbleDrawable,
|
||||
rippleMaskDrawable)
|
||||
ContextCompat.getColorStateList(context, R.color.mtrl_btn_ripple_color) ?: ColorStateList.valueOf(Color.TRANSPARENT),
|
||||
bubbleDrawable,
|
||||
rippleMaskDrawable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun renderMessageLayout(messageLayout: TimelineMessageLayout) {
|
||||
if (messageLayout !is TimelineMessageLayout.Bubble) {
|
||||
Timber.v("Can't render messageLayout $messageLayout")
|
||||
return
|
||||
(messageLayout as? TimelineMessageLayout.Bubble)
|
||||
?.updateDrawables()
|
||||
?.setConstraintsAndColor()
|
||||
?.toggleMessageOverlay()
|
||||
?.setPadding()
|
||||
?.setMargins()
|
||||
?.setAdditionalTopSpace()
|
||||
?: Timber.v("Can't render messageLayout $messageLayout")
|
||||
}
|
||||
|
||||
private fun TimelineMessageLayout.Bubble.updateDrawables() = apply {
|
||||
val shapeAppearanceModel = cornersRadius.shapeAppearanceModel()
|
||||
bubbleDrawable.apply {
|
||||
this.shapeAppearanceModel = shapeAppearanceModel
|
||||
this.fillColor = if (isPseudoBubble) {
|
||||
ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
} else {
|
||||
val backgroundColorAttr = if (isIncoming) R.attr.vctr_message_bubble_inbound else R.attr.vctr_message_bubble_outbound
|
||||
val backgroundColor = ThemeUtils.getColor(context, backgroundColorAttr)
|
||||
ColorStateList.valueOf(backgroundColor)
|
||||
}
|
||||
}
|
||||
updateDrawables(messageLayout)
|
||||
rippleMaskDrawable.shapeAppearanceModel = shapeAppearanceModel
|
||||
}
|
||||
|
||||
private fun TimelineMessageLayout.Bubble.setConstraintsAndColor() = apply {
|
||||
ConstraintSet().apply {
|
||||
clone(views.bubbleView)
|
||||
clear(R.id.viewStubContainer, ConstraintSet.END)
|
||||
if (messageLayout.timestampAsOverlay) {
|
||||
if (timestampAsOverlay) {
|
||||
val timeColor = ContextCompat.getColor(context, R.color.palette_white)
|
||||
views.messageTimeView.setTextColor(timeColor)
|
||||
connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0)
|
||||
|
@ -113,17 +136,26 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
|
|||
}
|
||||
applyTo(views.bubbleView)
|
||||
}
|
||||
if (messageLayout.timestampAsOverlay) {
|
||||
}
|
||||
|
||||
private fun TimelineMessageLayout.Bubble.toggleMessageOverlay() = apply {
|
||||
if (timestampAsOverlay) {
|
||||
views.messageOverlayView.isVisible = true
|
||||
(views.messageOverlayView.background as? GradientDrawable)?.cornerRadii = messageLayout.cornersRadius.toFloatArray()
|
||||
(views.messageOverlayView.background as? GradientDrawable)?.cornerRadii = cornersRadius.toFloatArray()
|
||||
} else {
|
||||
views.messageOverlayView.isVisible = false
|
||||
}
|
||||
if (messageLayout.isPseudoBubble && messageLayout.timestampAsOverlay) {
|
||||
}
|
||||
|
||||
private fun TimelineMessageLayout.Bubble.setPadding() = apply {
|
||||
if (isPseudoBubble && timestampAsOverlay) {
|
||||
views.viewStubContainer.root.setPadding(0, 0, 0, 0)
|
||||
} else {
|
||||
views.viewStubContainer.root.setPadding(horizontalStubPadding, verticalStubPadding, horizontalStubPadding, verticalStubPadding)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TimelineMessageLayout.Bubble.setMargins() = apply {
|
||||
if (isIncoming) {
|
||||
views.messageEndGuideline.updateLayoutParams<LayoutParams> {
|
||||
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
|
||||
|
@ -141,22 +173,11 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
|
|||
}
|
||||
}
|
||||
|
||||
private fun TimelineMessageLayout.Bubble.setAdditionalTopSpace() = apply {
|
||||
views.additionalTopSpace.isVisible = addTopMargin
|
||||
}
|
||||
|
||||
private fun TimelineMessageLayout.Bubble.CornersRadius.toFloatArray(): FloatArray {
|
||||
return floatArrayOf(topStartRadius, topStartRadius, topEndRadius, topEndRadius, bottomEndRadius, bottomEndRadius, bottomStartRadius, bottomStartRadius)
|
||||
}
|
||||
|
||||
private fun updateDrawables(messageLayout: TimelineMessageLayout.Bubble) {
|
||||
val shapeAppearanceModel = messageLayout.cornersRadius.shapeAppearanceModel()
|
||||
bubbleDrawable.apply {
|
||||
this.shapeAppearanceModel = shapeAppearanceModel
|
||||
this.fillColor = if (messageLayout.isPseudoBubble) {
|
||||
ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
} else {
|
||||
val backgroundColorAttr = if (isIncoming) R.attr.vctr_message_bubble_inbound else R.attr.vctr_message_bubble_outbound
|
||||
val backgroundColor = ThemeUtils.getColor(context, backgroundColorAttr)
|
||||
ColorStateList.valueOf(backgroundColor)
|
||||
}
|
||||
}
|
||||
rippleMaskDrawable.shapeAppearanceModel = shapeAppearanceModel
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import im.vector.app.features.themes.ThemeUtils
|
|||
abstract class RoomCategoryItem : VectorEpoxyModel<RoomCategoryItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute lateinit var title: String
|
||||
@EpoxyAttribute var itemCount: Int = 0
|
||||
@EpoxyAttribute var expanded: Boolean = false
|
||||
@EpoxyAttribute var unreadNotificationCount: Int = 0
|
||||
@EpoxyAttribute var showHighlighted: Boolean = false
|
||||
|
@ -46,14 +47,16 @@ abstract class RoomCategoryItem : VectorEpoxyModel<RoomCategoryItem.Holder>() {
|
|||
DrawableCompat.setTint(it, tintColor)
|
||||
}
|
||||
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
|
||||
holder.titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
|
||||
holder.titleView.text = title
|
||||
holder.counterView.text = itemCount.takeIf { it > 0 }?.toString().orEmpty()
|
||||
holder.counterView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
|
||||
holder.rootView.onClick(listener)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomCategoryUnreadCounterBadgeView)
|
||||
val titleView by bind<TextView>(R.id.roomCategoryTitleView)
|
||||
val counterView by bind<TextView>(R.id.roomCategoryCounterView)
|
||||
val rootView by bind<ViewGroup>(R.id.roomCategoryRootView)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -50,8 +52,10 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
|
|||
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
|
||||
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.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.extensions.orTrue
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
|
@ -287,6 +291,7 @@ class RoomListFragment @Inject constructor(
|
|||
))
|
||||
checkEmptyState()
|
||||
}
|
||||
observeItemCount(section, sectionAdapter)
|
||||
section.notificationCount.observe(viewLifecycleOwner) { counts ->
|
||||
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(
|
||||
notificationCount = counts.totalCount,
|
||||
|
@ -310,6 +315,7 @@ class RoomListFragment @Inject constructor(
|
|||
))
|
||||
checkEmptyState()
|
||||
}
|
||||
observeItemCount(section, sectionAdapter)
|
||||
section.isExpanded.observe(viewLifecycleOwner) { _ ->
|
||||
refreshCollapseStates()
|
||||
}
|
||||
|
@ -326,6 +332,7 @@ class RoomListFragment @Inject constructor(
|
|||
isLoading = false))
|
||||
checkEmptyState()
|
||||
}
|
||||
observeItemCount(section, sectionAdapter)
|
||||
section.notificationCount.observe(viewLifecycleOwner) { counts ->
|
||||
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(
|
||||
notificationCount = counts.totalCount,
|
||||
|
@ -373,6 +380,18 @@ class RoomListFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun observeItemCount(section: RoomsSection, sectionAdapter: SectionHeaderAdapter) {
|
||||
lifecycleScope.launch {
|
||||
section.itemCount
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.collect { count ->
|
||||
sectionAdapter.updateSection(
|
||||
sectionAdapter.roomsSectionData.copy(itemCount = count)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
|
||||
when (quickAction) {
|
||||
is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.app.features.invite.showInvites
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -72,7 +73,18 @@ class RoomListSectionBuilderGroup(
|
|||
session.getFilteredPagedRoomSummariesLive(qpm)
|
||||
.let { updatableFilterLivePageResult ->
|
||||
onUpdatable(updatableFilterLivePageResult)
|
||||
sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
|
||||
|
||||
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
|
||||
.flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
sections.add(
|
||||
RoomsSection(
|
||||
sectionName = name,
|
||||
livePages = updatableFilterLivePageResult.livePagedList,
|
||||
itemCount = itemCountFlow
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -109,9 +121,7 @@ class RoomListSectionBuilderGroup(
|
|||
.onEach { groupingMethod ->
|
||||
val selectedGroupId = (groupingMethod.orNull() as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary?.groupId
|
||||
activeGroupAwareQueries.onEach { updater ->
|
||||
updater.updateQuery { query ->
|
||||
query.copy(activeGroupId = selectedGroupId)
|
||||
}
|
||||
updater.queryParams = updater.queryParams.copy(activeGroupId = selectedGroupId)
|
||||
}
|
||||
}.launchIn(coroutineScope)
|
||||
|
||||
|
@ -265,7 +275,8 @@ class RoomListSectionBuilderGroup(
|
|||
RoomsSection(
|
||||
sectionName = name,
|
||||
livePages = livePagedList,
|
||||
notifyOfLocalEcho = notifyOfLocalEcho
|
||||
notifyOfLocalEcho = notifyOfLocalEcho,
|
||||
itemCount = session.getRoomCountFlow(roomQueryParams)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.flatMapLatest
|
|||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
|
@ -91,7 +92,18 @@ class RoomListSectionBuilderSpace(
|
|||
session.getFilteredPagedRoomSummariesLive(qpm)
|
||||
.let { updatableFilterLivePageResult ->
|
||||
onUpdatable(updatableFilterLivePageResult)
|
||||
sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
|
||||
|
||||
val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow()
|
||||
.flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
sections.add(
|
||||
RoomsSection(
|
||||
sectionName = name,
|
||||
livePages = updatableFilterLivePageResult.livePagedList,
|
||||
itemCount = itemCountFlow
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -261,7 +273,8 @@ class RoomListSectionBuilderSpace(
|
|||
RoomsSection(
|
||||
sectionName = stringProvider.getString(R.string.suggested_header),
|
||||
liveSuggested = liveSuggestedRooms,
|
||||
notifyOfLocalEcho = false
|
||||
notifyOfLocalEcho = false,
|
||||
itemCount = suggestedRoomsFlow.map { suggestions -> suggestions.size }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -338,11 +351,9 @@ class RoomListSectionBuilderSpace(
|
|||
RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> {
|
||||
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
|
||||
override fun updateForSpaceId(roomId: String?) {
|
||||
it.updateQuery {
|
||||
it.copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
|
||||
)
|
||||
}
|
||||
it.queryParams = roomQueryParams.copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -350,17 +361,13 @@ class RoomListSectionBuilderSpace(
|
|||
activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater {
|
||||
override fun updateForSpaceId(roomId: String?) {
|
||||
if (roomId != null) {
|
||||
it.updateQuery {
|
||||
it.copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
|
||||
)
|
||||
}
|
||||
it.queryParams = roomQueryParams.copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId)
|
||||
)
|
||||
} else {
|
||||
it.updateQuery {
|
||||
it.copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.None
|
||||
)
|
||||
}
|
||||
it.queryParams = roomQueryParams.copy(
|
||||
activeSpaceFilter = ActiveSpaceFilter.None
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -390,11 +397,19 @@ class RoomListSectionBuilderSpace(
|
|||
.flowOn(Dispatchers.Default)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
val itemCountFlow = livePagedList.asFlow()
|
||||
.flatMapLatest {
|
||||
val queryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
|
||||
session.getRoomCountFlow(queryParams)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
sections.add(
|
||||
RoomsSection(
|
||||
sectionName = name,
|
||||
livePages = livePagedList,
|
||||
notifyOfLocalEcho = notifyOfLocalEcho
|
||||
notifyOfLocalEcho = notifyOfLocalEcho,
|
||||
itemCount = itemCountFlow
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -192,8 +192,8 @@ class RoomListViewModel @AssistedInject constructor(
|
|||
roomFilter = action.filter
|
||||
)
|
||||
}
|
||||
updatableQuery?.updateQuery {
|
||||
it.copy(
|
||||
updatableQuery?.apply {
|
||||
queryParams = queryParams.copy(
|
||||
displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.NORMALIZED)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.features.home.room.list
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PagedList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
|
||||
|
||||
|
@ -29,6 +30,7 @@ data class RoomsSection(
|
|||
val liveList: LiveData<List<RoomSummary>>? = null,
|
||||
val liveSuggested: LiveData<SuggestedRoomInfo>? = null,
|
||||
val isExpanded: MutableLiveData<Boolean> = MutableLiveData(true),
|
||||
val itemCount: Flow<Int>,
|
||||
val notificationCount: MutableLiveData<RoomAggregateNotificationCount> = MutableLiveData(RoomAggregateNotificationCount(0, 0)),
|
||||
val notifyOfLocalEcho: Boolean = false
|
||||
)
|
||||
|
|
|
@ -33,6 +33,7 @@ class SectionHeaderAdapter constructor(
|
|||
|
||||
data class RoomsSectionData(
|
||||
val name: String,
|
||||
val itemCount: Int = 0,
|
||||
val isExpanded: Boolean = true,
|
||||
val notificationCount: Int = 0,
|
||||
val isHighlighted: Boolean = false,
|
||||
|
@ -85,8 +86,9 @@ class SectionHeaderAdapter constructor(
|
|||
val expandedArrowDrawable = ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also {
|
||||
DrawableCompat.setTint(it, tintColor)
|
||||
}
|
||||
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))
|
||||
binding.roomCategoryTitleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -39,7 +39,7 @@ class UrlMapProvider @Inject constructor(
|
|||
|
||||
suspend fun getMapUrl(): String {
|
||||
val upstreamMapUrl = tryOrNull { rawService.getElementWellknown(session.sessionParams) }
|
||||
?.mapTileServerConfig
|
||||
?.getBestMapTileServerConfig()
|
||||
?.mapStyleUrl
|
||||
return upstreamMapUrl ?: fallbackMapUrl
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ sealed class OnboardingAction : VectorViewModelAction {
|
|||
|
||||
data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction()
|
||||
|
||||
object PersonalizeProfile : OnboardingAction()
|
||||
data class UpdateDisplayName(val displayName: String) : OnboardingAction()
|
||||
object UpdateDisplayNameSkipped : OnboardingAction()
|
||||
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction()
|
||||
|
|
|
@ -51,9 +51,8 @@ sealed class OnboardingViewEvents : VectorViewEvents {
|
|||
object OnAccountCreated : OnboardingViewEvents()
|
||||
object OnAccountSignedIn : OnboardingViewEvents()
|
||||
object OnTakeMeHome : OnboardingViewEvents()
|
||||
object OnPersonalizeProfile : OnboardingViewEvents()
|
||||
object OnDisplayNameUpdated : OnboardingViewEvents()
|
||||
object OnDisplayNameSkipped : OnboardingViewEvents()
|
||||
object OnChooseDisplayName : OnboardingViewEvents()
|
||||
object OnChooseProfilePicture : OnboardingViewEvents()
|
||||
object OnPersonalizationComplete : OnboardingViewEvents()
|
||||
object OnBack : OnboardingViewEvents()
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ import im.vector.app.features.login.ReAuthHelper
|
|||
import im.vector.app.features.login.ServerType
|
||||
import im.vector.app.features.login.SignMode
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
|
@ -156,12 +157,13 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
is OnboardingAction.ResetAction -> handleResetAction(action)
|
||||
is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
|
||||
OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory()
|
||||
is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
|
||||
is OnboardingAction.UpdateDisplayName -> updateDisplayName(action.displayName)
|
||||
OnboardingAction.UpdateDisplayNameSkipped -> _viewEvents.post(OnboardingViewEvents.OnDisplayNameSkipped)
|
||||
OnboardingAction.UpdateProfilePictureSkipped -> _viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
|
||||
OnboardingAction.UpdateDisplayNameSkipped -> handleDisplayNameStepComplete()
|
||||
OnboardingAction.UpdateProfilePictureSkipped -> completePersonalization()
|
||||
OnboardingAction.PersonalizeProfile -> handlePersonalizeProfile()
|
||||
is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action)
|
||||
OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture()
|
||||
is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -762,15 +764,33 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
|
||||
authenticationService.reset()
|
||||
session.configureAndStart(applicationContext)
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Success(Unit)
|
||||
)
|
||||
}
|
||||
|
||||
when (isAccountCreated) {
|
||||
true -> _viewEvents.post(OnboardingViewEvents.OnAccountCreated)
|
||||
false -> _viewEvents.post(OnboardingViewEvents.OnAccountSignedIn)
|
||||
true -> {
|
||||
val personalizationState = createPersonalizationState(session, state)
|
||||
setState {
|
||||
copy(asyncLoginAction = Success(Unit), personalizationState = personalizationState)
|
||||
}
|
||||
_viewEvents.post(OnboardingViewEvents.OnAccountCreated)
|
||||
}
|
||||
false -> {
|
||||
setState { copy(asyncLoginAction = Success(Unit)) }
|
||||
_viewEvents.post(OnboardingViewEvents.OnAccountSignedIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createPersonalizationState(session: Session, state: OnboardingViewState): PersonalizationState {
|
||||
return when {
|
||||
vectorFeatures.isOnboardingPersonalizeEnabled() -> {
|
||||
val homeServerCapabilities = session.getHomeServerCapabilities()
|
||||
val capabilityOverrides = vectorOverrides.forceHomeserverCapabilities?.firstOrNull()
|
||||
state.personalizationState.copy(
|
||||
supportsChangingDisplayName = capabilityOverrides?.canChangeDisplayName ?: homeServerCapabilities.canChangeDisplayName,
|
||||
supportsChangingProfilePicture = capabilityOverrides?.canChangeAvatar ?: homeServerCapabilities.canChangeAvatar
|
||||
)
|
||||
}
|
||||
else -> state.personalizationState
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -910,7 +930,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
personalizationState = personalizationState.copy(displayName = displayName)
|
||||
)
|
||||
}
|
||||
_viewEvents.post(OnboardingViewEvents.OnDisplayNameUpdated)
|
||||
handleDisplayNameStepComplete()
|
||||
} catch (error: Throwable) {
|
||||
setState { copy(asyncDisplayName = Fail(error)) }
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(error))
|
||||
|
@ -918,12 +938,37 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handlePersonalizeProfile() {
|
||||
withPersonalisationState {
|
||||
when {
|
||||
it.supportsChangingDisplayName -> _viewEvents.post(OnboardingViewEvents.OnChooseDisplayName)
|
||||
it.supportsChangingProfilePicture -> _viewEvents.post(OnboardingViewEvents.OnChooseProfilePicture)
|
||||
else -> {
|
||||
throw IllegalStateException("It should not be possible to personalize without supporting display name or avatar changing")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDisplayNameStepComplete() {
|
||||
withPersonalisationState {
|
||||
when {
|
||||
it.supportsChangingProfilePicture -> _viewEvents.post(OnboardingViewEvents.OnChooseProfilePicture)
|
||||
else -> completePersonalization()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleProfilePictureSelected(action: OnboardingAction.ProfilePictureSelected) {
|
||||
setState {
|
||||
copy(personalizationState = personalizationState.copy(selectedPictureUri = action.uri))
|
||||
}
|
||||
}
|
||||
|
||||
private fun withPersonalisationState(block: (PersonalizationState) -> Unit) {
|
||||
withState { block(it.personalizationState) }
|
||||
}
|
||||
|
||||
private fun updateProfilePicture() {
|
||||
withState { state ->
|
||||
when (val pictureUri = state.personalizationState.selectedPictureUri) {
|
||||
|
@ -955,6 +1000,10 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onProfilePictureSaved() {
|
||||
completePersonalization()
|
||||
}
|
||||
|
||||
private fun completePersonalization() {
|
||||
_viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import com.airbnb.mvrx.Async
|
|||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import com.airbnb.mvrx.PersistState
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.app.features.login.LoginMode
|
||||
import im.vector.app.features.login.ServerType
|
||||
|
@ -83,10 +82,6 @@ data class OnboardingViewState(
|
|||
asyncDisplayName is Loading ||
|
||||
asyncProfilePicture is Loading
|
||||
}
|
||||
|
||||
fun isAuthTaskCompleted(): Boolean {
|
||||
return asyncLoginAction is Success
|
||||
}
|
||||
}
|
||||
|
||||
enum class OnboardingFlow {
|
||||
|
@ -97,6 +92,11 @@ enum class OnboardingFlow {
|
|||
|
||||
@Parcelize
|
||||
data class PersonalizationState(
|
||||
val supportsChangingDisplayName: Boolean = false,
|
||||
val supportsChangingProfilePicture: Boolean = false,
|
||||
val displayName: String? = null,
|
||||
val selectedPictureUri: Uri? = null
|
||||
) : Parcelable
|
||||
) : Parcelable {
|
||||
|
||||
fun supportsPersonalization() = supportsChangingDisplayName || supportsChangingProfilePicture
|
||||
}
|
||||
|
|
|
@ -20,11 +20,13 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.databinding.FragmentFtueAccountCreatedBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import javax.inject.Inject
|
||||
|
||||
class FtueAuthAccountCreatedFragment @Inject constructor(
|
||||
|
@ -42,8 +44,15 @@ class FtueAuthAccountCreatedFragment @Inject constructor(
|
|||
|
||||
private fun setupViews() {
|
||||
views.accountCreatedSubtitle.text = getString(R.string.ftue_account_created_subtitle, activeSessionHolder.getActiveSession().myUserId)
|
||||
views.accountCreatedPersonalize.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnPersonalizeProfile)) }
|
||||
views.accountCreatedPersonalize.debouncedClicks { viewModel.handle(OnboardingAction.PersonalizeProfile) }
|
||||
views.accountCreatedTakeMeHome.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) }
|
||||
views.accountCreatedTakeMeHomeCta.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) }
|
||||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
val canPersonalize = state.personalizationState.supportsPersonalization()
|
||||
views.personalizeButtonGroup.isVisible = canPersonalize
|
||||
views.takeMeHomeButtonGroup.isVisible = !canPersonalize
|
||||
}
|
||||
|
||||
override fun resetViewModel() {
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isInvisible
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
|
@ -70,6 +71,8 @@ class FtueAuthChooseProfilePictureFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun updateWithState(state: OnboardingViewState) {
|
||||
views.profilePictureToolbar.isInvisible = !state.personalizationState.supportsChangingDisplayName
|
||||
|
||||
val hasSetPicture = state.personalizationState.selectedPictureUri != null
|
||||
views.profilePictureSubmit.isEnabled = hasSetPicture
|
||||
views.changeProfilePictureIcon.setImageResource(if (hasSetPicture) R.drawable.ic_edit else R.drawable.ic_camera_plain)
|
||||
|
@ -93,4 +96,14 @@ class FtueAuthChooseProfilePictureFragment @Inject constructor(
|
|||
override fun resetViewModel() {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||
return when (withState(viewModel) { it.personalizationState.supportsChangingDisplayName }) {
|
||||
true -> super.onBackPressed(toolbarButton)
|
||||
false -> {
|
||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,17 +122,9 @@ class FtueAuthVariant(
|
|||
|
||||
private fun updateWithState(viewState: OnboardingViewState) {
|
||||
isForceLoginFallbackEnabled = viewState.isForceLoginFallbackEnabled
|
||||
views.loginLoading.isVisible = shouldShowLoading(viewState)
|
||||
views.loginLoading.isVisible = viewState.isLoading()
|
||||
}
|
||||
|
||||
private fun shouldShowLoading(viewState: OnboardingViewState) =
|
||||
if (vectorFeatures.isOnboardingPersonalizeEnabled()) {
|
||||
viewState.isLoading()
|
||||
} else {
|
||||
// Keep loading when during success because of the delay when switching to the next Activity
|
||||
viewState.isLoading() || viewState.isAuthTaskCompleted()
|
||||
}
|
||||
|
||||
override fun setIsLoading(isLoading: Boolean) = Unit
|
||||
|
||||
private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) {
|
||||
|
@ -230,12 +222,11 @@ class FtueAuthVariant(
|
|||
FtueAuthUseCaseFragment::class.java,
|
||||
option = commonOption)
|
||||
}
|
||||
OnboardingViewEvents.OnAccountCreated -> onAccountCreated()
|
||||
is OnboardingViewEvents.OnAccountCreated -> onAccountCreated()
|
||||
OnboardingViewEvents.OnAccountSignedIn -> onAccountSignedIn()
|
||||
OnboardingViewEvents.OnPersonalizeProfile -> onPersonalizeProfile()
|
||||
OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName()
|
||||
OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true)
|
||||
OnboardingViewEvents.OnDisplayNameUpdated -> onDisplayNameUpdated()
|
||||
OnboardingViewEvents.OnDisplayNameSkipped -> onDisplayNameUpdated()
|
||||
OnboardingViewEvents.OnChooseProfilePicture -> onChooseProfilePicture()
|
||||
OnboardingViewEvents.OnPersonalizationComplete -> navigateToHome(createdAccount = true)
|
||||
OnboardingViewEvents.OnBack -> activity.popBackstack()
|
||||
}.exhaustive
|
||||
|
@ -399,15 +390,11 @@ class FtueAuthVariant(
|
|||
}
|
||||
|
||||
private fun onAccountCreated() {
|
||||
if (vectorFeatures.isOnboardingPersonalizeEnabled()) {
|
||||
activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
activity.replaceFragment(
|
||||
views.loginFragmentContainer,
|
||||
FtueAuthAccountCreatedFragment::class.java,
|
||||
)
|
||||
} else {
|
||||
navigateToHome(createdAccount = true)
|
||||
}
|
||||
activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
activity.replaceFragment(
|
||||
views.loginFragmentContainer,
|
||||
FtueAuthAccountCreatedFragment::class.java
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateToHome(createdAccount: Boolean) {
|
||||
|
@ -416,14 +403,14 @@ class FtueAuthVariant(
|
|||
activity.finish()
|
||||
}
|
||||
|
||||
private fun onPersonalizeProfile() {
|
||||
private fun onChooseDisplayName() {
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
FtueAuthChooseDisplayNameFragment::class.java,
|
||||
option = commonOption
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDisplayNameUpdated() {
|
||||
private fun onChooseProfilePicture() {
|
||||
activity.addFragmentToBackstack(views.loginFragmentContainer,
|
||||
FtueAuthChooseProfilePictureFragment::class.java,
|
||||
option = commonOption
|
||||
|
|
|
@ -60,9 +60,9 @@ class CreatePollController @Inject constructor(
|
|||
pollTypeChangedListener { _, id ->
|
||||
host.callback?.onPollTypeChanged(
|
||||
if (id == R.id.openPollTypeRadioButton) {
|
||||
PollType.DISCLOSED
|
||||
PollType.DISCLOSED_UNSTABLE
|
||||
} else {
|
||||
PollType.UNDISCLOSED
|
||||
PollType.UNDISCLOSED_UNSTABLE
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -71,9 +71,10 @@ class CreatePollViewModel @AssistedInject constructor(
|
|||
val event = room.getTimelineEvent(eventId) ?: return
|
||||
val content = event.getLastMessageContent() as? MessagePollContent ?: return
|
||||
|
||||
val pollType = content.pollCreationInfo?.kind ?: PollType.DISCLOSED
|
||||
val question = content.pollCreationInfo?.question?.question ?: ""
|
||||
val options = content.pollCreationInfo?.answers?.mapNotNull { it.answer } ?: List(MIN_OPTIONS_COUNT) { "" }
|
||||
val pollCreationInfo = content.getBestPollCreationInfo()
|
||||
val pollType = pollCreationInfo?.kind ?: PollType.DISCLOSED_UNSTABLE
|
||||
val question = pollCreationInfo?.question?.getBestQuestion() ?: ""
|
||||
val options = pollCreationInfo?.answers?.mapNotNull { it.getBestAnswer() } ?: List(MIN_OPTIONS_COUNT) { "" }
|
||||
|
||||
setState {
|
||||
copy(
|
||||
|
|
|
@ -27,7 +27,7 @@ data class CreatePollViewState(
|
|||
val options: List<String> = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" },
|
||||
val canCreatePoll: Boolean = false,
|
||||
val canAddMoreOptions: Boolean = true,
|
||||
val pollType: PollType = PollType.DISCLOSED
|
||||
val pollType: PollType = PollType.DISCLOSED_UNSTABLE
|
||||
) : MavericksState {
|
||||
|
||||
constructor(args: CreatePollArgs) : this(
|
||||
|
|
|
@ -28,7 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.message.PollType
|
|||
abstract class PollTypeSelectionItem : VectorEpoxyModel<PollTypeSelectionItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var pollType: PollType = PollType.DISCLOSED
|
||||
var pollType: PollType = PollType.DISCLOSED_UNSTABLE
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var pollTypeChangedListener: RadioGroup.OnCheckedChangeListener? = null
|
||||
|
@ -38,8 +38,8 @@ abstract class PollTypeSelectionItem : VectorEpoxyModel<PollTypeSelectionItem.Ho
|
|||
|
||||
holder.pollTypeRadioGroup.check(
|
||||
when (pollType) {
|
||||
PollType.DISCLOSED -> R.id.openPollTypeRadioButton
|
||||
PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton
|
||||
PollType.DISCLOSED_UNSTABLE, PollType.DISCLOSED -> R.id.openPollTypeRadioButton
|
||||
PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -38,8 +38,13 @@ data class ElementWellKnown(
|
|||
val riotE2E: E2EWellKnownConfig? = null,
|
||||
|
||||
@Json(name = "org.matrix.msc3488.tile_server")
|
||||
val unstableMapTileServerConfig: MapTileServerConfig? = null,
|
||||
|
||||
@Json(name = "m.tile_server")
|
||||
val mapTileServerConfig: MapTileServerConfig? = null
|
||||
)
|
||||
) {
|
||||
fun getBestMapTileServerConfig() = mapTileServerConfig ?: unstableMapTileServerConfig
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class E2EWellKnownConfig(
|
||||
|
|
|
@ -94,6 +94,12 @@ class AddRoomListController @Inject constructor(
|
|||
}
|
||||
|
||||
var totalSize: Int = 0
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
field = value
|
||||
requestForcedModelBuild()
|
||||
}
|
||||
}
|
||||
|
||||
var selectedItems: Map<String, Boolean> = emptyMap()
|
||||
set(value) {
|
||||
|
@ -120,7 +126,8 @@ class AddRoomListController @Inject constructor(
|
|||
add(
|
||||
RoomCategoryItem_().apply {
|
||||
id("header")
|
||||
title(host.sectionName ?: "")
|
||||
title(host.sectionName.orEmpty())
|
||||
itemCount(host.totalSize)
|
||||
expanded(host.expanded)
|
||||
listener {
|
||||
host.expanded = !host.expanded
|
||||
|
|
|
@ -22,6 +22,8 @@ import android.view.Menu
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -35,9 +37,12 @@ import im.vector.app.core.extensions.cleanup
|
|||
import im.vector.app.core.platform.OnBackPressed
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.databinding.FragmentSpaceAddRoomsBinding
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import reactivecircus.flowbinding.appcompat.queryTextChanges
|
||||
import javax.inject.Inject
|
||||
|
@ -169,48 +174,63 @@ class SpaceAddRoomFragment @Inject constructor(
|
|||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
val concatAdapter = ConcatAdapter()
|
||||
spaceEpoxyController.sectionName = getString(R.string.spaces_header)
|
||||
roomEpoxyController.sectionName = getString(R.string.rooms_header)
|
||||
spaceEpoxyController.listener = this
|
||||
roomEpoxyController.listener = this
|
||||
setupSpaceSection()
|
||||
setupRoomSection()
|
||||
setupDmSection()
|
||||
|
||||
viewModel.updatableLiveSpacePageResult.liveBoundaries.observe(viewLifecycleOwner) {
|
||||
views.roomList.adapter = ConcatAdapter().apply {
|
||||
addAdapter(roomEpoxyController.adapter)
|
||||
addAdapter(spaceEpoxyController.adapter)
|
||||
addAdapter(dmEpoxyController.adapter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSpaceSection() {
|
||||
spaceEpoxyController.sectionName = getString(R.string.spaces_header)
|
||||
spaceEpoxyController.listener = this
|
||||
viewModel.spaceUpdatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
|
||||
spaceEpoxyController.boundaryChange(it)
|
||||
}
|
||||
viewModel.updatableLiveSpacePageResult.livePagedList.observe(viewLifecycleOwner) {
|
||||
spaceEpoxyController.totalSize = it.size
|
||||
viewModel.spaceUpdatableLivePageResult.livePagedList.observe(viewLifecycleOwner) {
|
||||
spaceEpoxyController.submitList(it)
|
||||
}
|
||||
listenItemCount(viewModel.spaceCountFlow) { spaceEpoxyController.totalSize = it }
|
||||
}
|
||||
|
||||
viewModel.updatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
|
||||
private fun setupRoomSection() {
|
||||
roomEpoxyController.sectionName = getString(R.string.rooms_header)
|
||||
roomEpoxyController.listener = this
|
||||
|
||||
viewModel.roomUpdatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
|
||||
roomEpoxyController.boundaryChange(it)
|
||||
}
|
||||
viewModel.updatableLivePageResult.livePagedList.observe(viewLifecycleOwner) {
|
||||
roomEpoxyController.totalSize = it.size
|
||||
viewModel.roomUpdatableLivePageResult.livePagedList.observe(viewLifecycleOwner) {
|
||||
roomEpoxyController.submitList(it)
|
||||
}
|
||||
|
||||
listenItemCount(viewModel.roomCountFlow) { roomEpoxyController.totalSize = it }
|
||||
views.roomList.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
views.roomList.setHasFixedSize(true)
|
||||
}
|
||||
|
||||
concatAdapter.addAdapter(roomEpoxyController.adapter)
|
||||
concatAdapter.addAdapter(spaceEpoxyController.adapter)
|
||||
|
||||
private fun setupDmSection() {
|
||||
// This controller can be disabled depending on the space type (public or not)
|
||||
viewModel.updatableDMLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
|
||||
dmEpoxyController.boundaryChange(it)
|
||||
}
|
||||
viewModel.updatableDMLivePageResult.livePagedList.observe(viewLifecycleOwner) {
|
||||
dmEpoxyController.totalSize = it.size
|
||||
dmEpoxyController.submitList(it)
|
||||
}
|
||||
dmEpoxyController.sectionName = getString(R.string.direct_chats_header)
|
||||
dmEpoxyController.listener = this
|
||||
viewModel.dmUpdatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) {
|
||||
dmEpoxyController.boundaryChange(it)
|
||||
}
|
||||
viewModel.dmUpdatableLivePageResult.livePagedList.observe(viewLifecycleOwner) {
|
||||
dmEpoxyController.submitList(it)
|
||||
}
|
||||
listenItemCount(viewModel.dmCountFlow) { dmEpoxyController.totalSize = it }
|
||||
}
|
||||
|
||||
concatAdapter.addAdapter(dmEpoxyController.adapter)
|
||||
|
||||
views.roomList.adapter = concatAdapter
|
||||
private fun listenItemCount(itemCountFlow: Flow<Int>, onEachAction: (Int) -> Unit) {
|
||||
lifecycleScope.launch {
|
||||
itemCountFlow
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
||||
.collect { count -> onEachAction(count) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
package im.vector.app.features.spaces.manage
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.paging.PagedList
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
|
@ -30,6 +30,9 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
|||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
|
@ -60,7 +63,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
|
|||
|
||||
companion object : MavericksViewModelFactory<SpaceAddRoomsViewModel, SpaceAddRoomsState> by hiltMavericksViewModelFactory()
|
||||
|
||||
val updatableLiveSpacePageResult: UpdatableLivePageResult by lazy {
|
||||
val spaceUpdatableLivePageResult: UpdatableLivePageResult by lazy {
|
||||
session.getFilteredPagedRoomSummariesLive(
|
||||
roomSummaryQueryParams {
|
||||
this.memberships = listOf(Membership.JOIN)
|
||||
|
@ -79,7 +82,13 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
val updatableLivePageResult: UpdatableLivePageResult by lazy {
|
||||
val spaceCountFlow: Flow<Int> by lazy {
|
||||
spaceUpdatableLivePageResult.livePagedList.asFlow()
|
||||
.flatMapLatest { session.getRoomCountFlow(spaceUpdatableLivePageResult.queryParams) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
val roomUpdatableLivePageResult: UpdatableLivePageResult by lazy {
|
||||
session.getFilteredPagedRoomSummariesLive(
|
||||
roomSummaryQueryParams {
|
||||
this.memberships = listOf(Membership.JOIN)
|
||||
|
@ -99,7 +108,13 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
val updatableDMLivePageResult: UpdatableLivePageResult by lazy {
|
||||
val roomCountFlow: Flow<Int> by lazy {
|
||||
roomUpdatableLivePageResult.livePagedList.asFlow()
|
||||
.flatMapLatest { session.getRoomCountFlow(roomUpdatableLivePageResult.queryParams) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
val dmUpdatableLivePageResult: UpdatableLivePageResult by lazy {
|
||||
session.getFilteredPagedRoomSummariesLive(
|
||||
roomSummaryQueryParams {
|
||||
this.memberships = listOf(Membership.JOIN)
|
||||
|
@ -119,6 +134,12 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
val dmCountFlow: Flow<Int> by lazy {
|
||||
dmUpdatableLivePageResult.livePagedList.asFlow()
|
||||
.flatMapLatest { session.getRoomCountFlow(dmUpdatableLivePageResult.queryParams) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private val selectionList = mutableMapOf<String, Boolean>()
|
||||
val selectionListLiveData = MutableLiveData<Map<String, Boolean>>()
|
||||
|
||||
|
@ -143,17 +164,13 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
|
|||
|
||||
override fun handle(action: SpaceAddRoomActions) {
|
||||
when (action) {
|
||||
is SpaceAddRoomActions.UpdateFilter -> {
|
||||
updatableLivePageResult.updateQuery {
|
||||
it.copy(
|
||||
displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE)
|
||||
)
|
||||
}
|
||||
updatableLiveSpacePageResult.updateQuery {
|
||||
it.copy(
|
||||
displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE)
|
||||
)
|
||||
}
|
||||
is SpaceAddRoomActions.UpdateFilter -> {
|
||||
roomUpdatableLivePageResult.queryParams = roomUpdatableLivePageResult.queryParams.copy(
|
||||
displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE)
|
||||
)
|
||||
roomUpdatableLivePageResult.queryParams = roomUpdatableLivePageResult.queryParams.copy(
|
||||
displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE)
|
||||
)
|
||||
setState {
|
||||
copy(
|
||||
currentFilter = action.filter
|
||||
|
@ -164,7 +181,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
|
|||
selectionList[action.roomSummary.roomId] = (selectionList[action.roomSummary.roomId] ?: false).not()
|
||||
selectionListLiveData.postValue(selectionList.toMap())
|
||||
}
|
||||
SpaceAddRoomActions.Save -> {
|
||||
SpaceAddRoomActions.Save -> {
|
||||
doAddSelectedRooms()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,14 @@
|
|||
app:layout_constraintBottom_toTopOf="@id/accountCreatedPersonalize"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountCreatedSubtitle" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/personalizeButtonGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="accountCreatedPersonalize,accountCreatedTakeMeHome"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/accountCreatedPersonalize"
|
||||
style="@style/Widget.Vector.Button.Login"
|
||||
|
@ -96,11 +104,10 @@
|
|||
android:textAllCaps="true"
|
||||
android:textColor="?colorSecondary"
|
||||
android:transitionName="loginSubmitTransition"
|
||||
app:layout_constraintBottom_toTopOf="@id/accountCreatedSpace5"
|
||||
app:layout_constraintBottom_toTopOf="@id/accountCreatedTakeMeHome"
|
||||
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountCreatedSpace4"
|
||||
tools:text="@string/ftue_account_created_personalize" />
|
||||
app:layout_constraintTop_toBottomOf="@id/accountCreatedSpace4" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/accountCreatedTakeMeHome"
|
||||
|
@ -111,17 +118,46 @@
|
|||
android:textAllCaps="true"
|
||||
android:textColor="@color/element_background_light"
|
||||
android:transitionName="loginSubmitTransition"
|
||||
app:layout_constraintBottom_toTopOf="@id/accountCreatedSpace5"
|
||||
app:layout_constraintBottom_toTopOf="@id/ctaBottomBarrier"
|
||||
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountCreatedPersonalize" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/takeMeHomeButtonGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="accountCreatedTakeMeHomeCta" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/accountCreatedTakeMeHomeCta"
|
||||
style="@style/Widget.Vector.Button.Login"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/element_background_light"
|
||||
android:text="@string/ftue_account_created_take_me_home"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?colorSecondary"
|
||||
android:transitionName="loginSubmitTransition"
|
||||
app:layout_constraintBottom_toTopOf="@id/ctaBottomBarrier"
|
||||
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountCreatedSpace4" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/ctaBottomBarrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="accountCreatedTakeMeHomeCta,accountCreatedTakeMeHome" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/accountCreatedSpace5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintHeight_percent="0.05"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountCreatedPersonalize" />
|
||||
app:layout_constraintTop_toBottomOf="@id/ctaBottomBarrier" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
style="@style/Widget.Vector.Toolbar.Settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toTopOf="@id/profilePictureView"
|
||||
app:layout_constraintTop_toBottomOf="@id/profilePictureToolbar"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
|
|
@ -1,36 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/roomCategoryRootView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:colorBackground"
|
||||
android:background="?attr/vctr_header_background"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="4dp">
|
||||
android:paddingHorizontal="@dimen/layout_horizontal_margin"
|
||||
android:paddingVertical="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/roomCategoryTitleView"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="wrap_content"
|
||||
style="@style/Widget.Vector.TextView.Subtitle.Medium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
android:textStyle="bold"
|
||||
app:drawableTint="?vctr_content_secondary"
|
||||
tools:drawableEnd="@drawable/ic_expand_more"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?vctr_content_primary"
|
||||
app:layout_constraintEnd_toStartOf="@id/roomCategoryCounterView"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth="wrap_content_constrained"
|
||||
tools:text="@string/room_participants_header_direct_chats" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/roomCategoryCounterView"
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:drawablePadding="2dp"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
app:drawableTint="?vctr_content_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="@id/roomCategoryTitleView"
|
||||
app:layout_constraintEnd_toStartOf="@id/roomCategoryUnreadCounterBadgeView"
|
||||
app:layout_constraintStart_toEndOf="@id/roomCategoryTitleView"
|
||||
app:layout_constraintTop_toTopOf="@id/roomCategoryTitleView"
|
||||
app:layout_constraintWidth="wrap_content_constrained"
|
||||
tools:drawableEnd="@drawable/ic_expand_more"
|
||||
tools:text="14" />
|
||||
|
||||
<im.vector.app.features.home.room.list.UnreadCounterBadgeView
|
||||
android:id="@+id/roomCategoryUnreadCounterBadgeView"
|
||||
style="@style/Widget.Vector.TextView.Micro"
|
||||
|
@ -39,10 +59,12 @@
|
|||
android:gravity="center"
|
||||
android:minWidth="16dp"
|
||||
android:minHeight="16dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:textColor="?colorOnError"
|
||||
app:layout_constraintBottom_toBottomOf="@id/roomCategoryTitleView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/roomCategoryTitleView"
|
||||
tools:background="@drawable/bg_unread_highlight"
|
||||
tools:text="24" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.amshove.kluent.shouldBeTrue
|
|||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.room.model.message.LocationAsset
|
||||
import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
|
||||
|
||||
class LocationDataTest {
|
||||
|
@ -64,13 +65,24 @@ class LocationDataTest {
|
|||
|
||||
@Test
|
||||
fun selfLocationTest() {
|
||||
val contentWithNullAsset = MessageLocationContent(body = "", geoUri = "", locationAsset = null)
|
||||
val contentWithNullAsset = MessageLocationContent(body = "", geoUri = "")
|
||||
contentWithNullAsset.isSelfLocation().shouldBeTrue()
|
||||
|
||||
val contentWithNullAssetType = MessageLocationContent(body = "", geoUri = "", locationAsset = LocationAsset(type = null))
|
||||
val contentWithNullAssetType = MessageLocationContent(body = "", geoUri = "", unstableLocationAsset = LocationAsset(type = null))
|
||||
contentWithNullAssetType.isSelfLocation().shouldBeTrue()
|
||||
|
||||
val contentWithSelfAssetType = MessageLocationContent(body = "", geoUri = "", locationAsset = LocationAsset(type = LocationAssetType.SELF))
|
||||
val contentWithSelfAssetType = MessageLocationContent(body = "", geoUri = "", unstableLocationAsset = LocationAsset(type = LocationAssetType.SELF))
|
||||
contentWithSelfAssetType.isSelfLocation().shouldBeTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unstablePrefixTest() {
|
||||
val geoUri = "geo :12.34,56.78;13.56"
|
||||
|
||||
val contentWithUnstablePrefixes = MessageLocationContent(body = "", geoUri = "", unstableLocationInfo = LocationInfo(geoUri = geoUri))
|
||||
contentWithUnstablePrefixes.getBestLocationInfo()?.geoUri.shouldBeEqualTo(geoUri)
|
||||
|
||||
val contentWithStablePrefixes = MessageLocationContent(body = "", geoUri = "", locationInfo = LocationInfo(geoUri = geoUri))
|
||||
contentWithStablePrefixes.getBestLocationInfo()?.geoUri.shouldBeEqualTo(geoUri)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ import android.net.Uri
|
|||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.test.MvRxTestRule
|
||||
import im.vector.app.features.DefaultVectorOverrides
|
||||
import im.vector.app.features.login.ReAuthHelper
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import im.vector.app.test.fakes.FakeAnalyticsTracker
|
||||
|
@ -29,20 +29,27 @@ import im.vector.app.test.fakes.FakeAuthenticationService
|
|||
import im.vector.app.test.fakes.FakeContext
|
||||
import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory
|
||||
import im.vector.app.test.fakes.FakeHomeServerHistoryService
|
||||
import im.vector.app.test.fakes.FakeRegistrationWizard
|
||||
import im.vector.app.test.fakes.FakeSession
|
||||
import im.vector.app.test.fakes.FakeStringProvider
|
||||
import im.vector.app.test.fakes.FakeUri
|
||||
import im.vector.app.test.fakes.FakeUriFilenameResolver
|
||||
import im.vector.app.test.fakes.FakeVectorFeatures
|
||||
import im.vector.app.test.fakes.FakeVectorOverrides
|
||||
import im.vector.app.test.test
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||
|
||||
private const val A_DISPLAY_NAME = "a display name"
|
||||
private const val A_PICTURE_FILENAME = "a-picture.png"
|
||||
private val AN_ERROR = RuntimeException("an error!")
|
||||
private val AN_UNSUPPORTED_PERSONALISATION_STATE = PersonalizationState(
|
||||
supportsChangingDisplayName = false,
|
||||
supportsChangingProfilePicture = false
|
||||
)
|
||||
|
||||
class OnboardingViewModelTest {
|
||||
|
||||
|
@ -55,6 +62,7 @@ class OnboardingViewModelTest {
|
|||
private val fakeSession = FakeSession()
|
||||
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
|
||||
private val fakeAuthenticationService = FakeAuthenticationService()
|
||||
|
||||
lateinit var viewModel: OnboardingViewModel
|
||||
|
||||
|
@ -75,21 +83,84 @@ class OnboardingViewModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `when handling display name update then updates upstream user display name`() = runBlockingTest {
|
||||
fun `given supports changing display name when handling PersonalizeProfile then emits contents choose display name`() = runBlockingTest {
|
||||
val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = true, supportsChangingProfilePicture = false))
|
||||
viewModel = createViewModel(initialState)
|
||||
val test = viewModel.test(this)
|
||||
|
||||
viewModel.handle(OnboardingAction.PersonalizeProfile)
|
||||
|
||||
test
|
||||
.assertEvents(OnboardingViewEvents.OnChooseDisplayName)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given only supports changing profile picture when handling PersonalizeProfile then emits contents choose profile picture`() = runBlockingTest {
|
||||
val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = false, supportsChangingProfilePicture = true))
|
||||
viewModel = createViewModel(initialState)
|
||||
val test = viewModel.test(this)
|
||||
|
||||
viewModel.handle(OnboardingAction.PersonalizeProfile)
|
||||
|
||||
test
|
||||
.assertEvents(OnboardingViewEvents.OnChooseProfilePicture)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given homeserver does not support personalisation when registering account then updates state and emits account created event`() = runBlockingTest {
|
||||
fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(HomeServerCapabilities(canChangeDisplayName = false, canChangeAvatar = false))
|
||||
givenSuccessfullyCreatesAccount()
|
||||
val test = viewModel.test(this)
|
||||
|
||||
viewModel.handle(OnboardingAction.RegisterDummy)
|
||||
|
||||
test
|
||||
.assertStates(
|
||||
initialState,
|
||||
initialState.copy(asyncRegistration = Loading()),
|
||||
initialState.copy(
|
||||
asyncLoginAction = Success(Unit),
|
||||
asyncRegistration = Loading(),
|
||||
personalizationState = AN_UNSUPPORTED_PERSONALISATION_STATE
|
||||
),
|
||||
initialState.copy(
|
||||
asyncLoginAction = Success(Unit),
|
||||
asyncRegistration = Uninitialized,
|
||||
personalizationState = AN_UNSUPPORTED_PERSONALISATION_STATE
|
||||
)
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.OnAccountCreated)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given changing profile picture is supported when updating display name then updates upstream user display name and moves to choose profile picture`() = runBlockingTest {
|
||||
val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = true))
|
||||
viewModel = createViewModel(personalisedInitialState)
|
||||
val test = viewModel.test(this)
|
||||
|
||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||
|
||||
test
|
||||
.assertStates(
|
||||
initialState,
|
||||
initialState.copy(asyncDisplayName = Loading()),
|
||||
initialState.copy(
|
||||
asyncDisplayName = Success(Unit),
|
||||
personalizationState = initialState.personalizationState.copy(displayName = A_DISPLAY_NAME)
|
||||
)
|
||||
)
|
||||
.assertEvents(OnboardingViewEvents.OnDisplayNameUpdated)
|
||||
.assertStates(expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState))
|
||||
.assertEvents(OnboardingViewEvents.OnChooseProfilePicture)
|
||||
.finish()
|
||||
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given changing profile picture is not supported when updating display name then updates upstream user display name and completes personalization`() = runBlockingTest {
|
||||
val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = false))
|
||||
viewModel = createViewModel(personalisedInitialState)
|
||||
val test = viewModel.test(this)
|
||||
|
||||
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
|
||||
|
||||
test
|
||||
.assertStates(expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState))
|
||||
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
|
||||
.finish()
|
||||
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
|
||||
}
|
||||
|
@ -184,7 +255,7 @@ class OnboardingViewModelTest {
|
|||
return OnboardingViewModel(
|
||||
state,
|
||||
fakeContext.instance,
|
||||
FakeAuthenticationService(),
|
||||
fakeAuthenticationService,
|
||||
fakeActiveSessionHolder.instance,
|
||||
FakeHomeServerConnectionConfigFactory().instance,
|
||||
ReAuthHelper(),
|
||||
|
@ -193,7 +264,7 @@ class OnboardingViewModelTest {
|
|||
FakeVectorFeatures(),
|
||||
FakeAnalyticsTracker(),
|
||||
fakeUriFilenameResolver.instance,
|
||||
DefaultVectorOverrides()
|
||||
FakeVectorOverrides()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -214,4 +285,23 @@ class OnboardingViewModelTest {
|
|||
state.copy(asyncProfilePicture = Loading()),
|
||||
state.copy(asyncProfilePicture = Fail(cause))
|
||||
)
|
||||
|
||||
private fun givenSuccessfullyCreatesAccount() {
|
||||
fakeActiveSessionHolder.expectSetsActiveSession(fakeSession)
|
||||
val registrationWizard = FakeRegistrationWizard().also { it.givenSuccessfulDummy(fakeSession) }
|
||||
fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
|
||||
fakeAuthenticationService.expectReset()
|
||||
fakeSession.expectStartsSyncing()
|
||||
}
|
||||
|
||||
private fun expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState: OnboardingViewState): List<OnboardingViewState> {
|
||||
return listOf(
|
||||
personalisedInitialState,
|
||||
personalisedInitialState.copy(asyncDisplayName = Loading()),
|
||||
personalisedInitialState.copy(
|
||||
asyncDisplayName = Success(Unit),
|
||||
personalizationState = personalisedInitialState.personalizationState.copy(displayName = A_DISPLAY_NAME)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ package im.vector.app.test.fakes
|
|||
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import io.mockk.every
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
||||
class FakeActiveSessionHolder(
|
||||
private val fakeSession: FakeSession = FakeSession()
|
||||
|
@ -26,4 +28,8 @@ class FakeActiveSessionHolder(
|
|||
val instance = mockk<ActiveSessionHolder> {
|
||||
every { getActiveSession() } returns fakeSession
|
||||
}
|
||||
|
||||
fun expectSetsActiveSession(session: Session) {
|
||||
justRun { instance.setActiveSession(session) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,18 @@
|
|||
|
||||
package im.vector.app.test.fakes
|
||||
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
||||
|
||||
class FakeAuthenticationService : AuthenticationService by mockk()
|
||||
class FakeAuthenticationService : AuthenticationService by mockk() {
|
||||
fun givenRegistrationWizard(registrationWizard: RegistrationWizard) {
|
||||
every { getRegistrationWizard() } returns registrationWizard
|
||||
}
|
||||
|
||||
fun expectReset() {
|
||||
coJustRun { reset() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.test.fakes
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||
|
||||
class FakeHomeServerCapabilitiesService : HomeServerCapabilitiesService by mockk() {
|
||||
|
||||
fun givenCapabilities(homeServerCapabilities: HomeServerCapabilities) {
|
||||
every { getHomeServerCapabilities() } returns homeServerCapabilities
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.test.fakes
|
||||
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
||||
class FakeRegistrationWizard : RegistrationWizard by mockk() {
|
||||
|
||||
fun givenSuccessfulDummy(session: Session) {
|
||||
coEvery { dummy() } returns RegistrationResult.Success(session)
|
||||
}
|
||||
}
|
|
@ -17,10 +17,13 @@
|
|||
package im.vector.app.test.fakes
|
||||
|
||||
import android.net.Uri
|
||||
import im.vector.app.core.extensions.configureAndStart
|
||||
import im.vector.app.core.extensions.startSyncing
|
||||
import im.vector.app.core.extensions.vectorStore
|
||||
import im.vector.app.features.session.VectorSessionStore
|
||||
import im.vector.app.test.testCoroutineDispatchers
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -28,6 +31,7 @@ import org.matrix.android.sdk.api.session.Session
|
|||
class FakeSession(
|
||||
val fakeCryptoService: FakeCryptoService = FakeCryptoService(),
|
||||
val fakeProfileService: FakeProfileService = FakeProfileService(),
|
||||
val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(),
|
||||
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService()
|
||||
) : Session by mockk(relaxed = true) {
|
||||
|
||||
|
@ -42,6 +46,7 @@ class FakeSession(
|
|||
override val coroutineDispatchers = testCoroutineDispatchers
|
||||
override suspend fun setDisplayName(userId: String, newDisplayName: String) = fakeProfileService.setDisplayName(userId, newDisplayName)
|
||||
override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) = fakeProfileService.updateAvatar(userId, newAvatarUri, fileName)
|
||||
override fun getHomeServerCapabilities() = fakeHomeServerCapabilitiesService.getHomeServerCapabilities()
|
||||
|
||||
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
|
||||
coEvery {
|
||||
|
@ -50,4 +55,11 @@ class FakeSession(
|
|||
vectorSessionStore
|
||||
}
|
||||
}
|
||||
|
||||
fun expectStartsSyncing() {
|
||||
coJustRun {
|
||||
this@FakeSession.configureAndStart(any(), startSyncing = true)
|
||||
this@FakeSession.startSyncing(any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.test.fakes
|
||||
|
||||
import im.vector.app.features.DefaultVectorOverrides
|
||||
import im.vector.app.features.VectorOverrides
|
||||
|
||||
class FakeVectorOverrides : VectorOverrides by DefaultVectorOverrides()
|
Loading…
Reference in a new issue