* Implement Presence Service:

- Get Presence Status
     - Set Presence Status
* Integrate presence in room details screen
* Integrate presence in room people's view
* Update UI to support presence
* Fix bug when insertOrUpdate was called on RoomMemberEventHandler and override the correct presence value in RoomMemberSummaryEntity
* Improve performance on updateUserPresence in RoomMemberSummaryEntity entity
* Remarks & linter fixes
* Disable presence when there is no m.presence events. In some servers like matrix.org is disabled atm.
* Enhance UI Presence on DM room lists to support dark/light theme
* Restore missing lines in gradle.properties to speed up debugging
This commit is contained in:
ariskotsomitopoulos 2021-09-17 17:14:11 +03:00
parent 58b69b1fe4
commit 9ab59a543d
66 changed files with 1031 additions and 67 deletions

View file

@ -23,3 +23,6 @@ vector.debugPrivateData=false
# httpLogLevel values: NONE, BASIC, HEADERS, BODY # httpLogLevel values: NONE, BASIC, HEADERS, BODY
vector.httpLogLevel=BASIC vector.httpLogLevel=BASIC
# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
#vector.debugPrivateData=true
#vector.httpLogLevel=BODY

View file

@ -133,4 +133,8 @@
<color name="vctr_voice_message_toast_background_light">@color/palette_black_900</color> <color name="vctr_voice_message_toast_background_light">@color/palette_black_900</color>
<color name="vctr_voice_message_toast_background_dark">@color/palette_gray_400</color> <color name="vctr_voice_message_toast_background_dark">@color/palette_gray_400</color>
<!-- Presence Indicator colors -->
<attr name="vctr_presence_indicator_offline" format="color" />
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color>
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
</resources> </resources>

View file

@ -42,6 +42,9 @@
<item name="vctr_markdown_block_background_color">@android:color/black</item> <item name="vctr_markdown_block_background_color">@android:color/black</item>
<item name="vctr_spoiler_background_color">#FFFFFFFF</item> <item name="vctr_spoiler_background_color">#FFFFFFFF</item>
<!-- Presence Indicator colors -->
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_dark</item>
<!-- Some alias --> <!-- Some alias -->
<item name="vctr_header_background">?vctr_system</item> <item name="vctr_header_background">?vctr_system</item>
<item name="vctr_list_separator">?vctr_content_quinary</item> <item name="vctr_list_separator">?vctr_content_quinary</item>

View file

@ -42,6 +42,9 @@
<item name="vctr_markdown_block_background_color">#FFEEEEEE</item> <item name="vctr_markdown_block_background_color">#FFEEEEEE</item>
<item name="vctr_spoiler_background_color">#FF000000</item> <item name="vctr_spoiler_background_color">#FF000000</item>
<!-- Presence Indicator colors -->
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_light</item>
<!-- Some alias --> <!-- Some alias -->
<item name="vctr_header_background">?vctr_system</item> <item name="vctr_header_background">?vctr_system</item>
<item name="vctr_list_separator">?vctr_content_quinary</item> <item name="vctr_list_separator">?vctr_content_quinary</item>

View file

@ -42,6 +42,7 @@ import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerS
import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.media.MediaService
import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.openid.OpenIdService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.presence.PresenceService
import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.pushers.PushersService import org.matrix.android.sdk.api.session.pushers.PushersService
import org.matrix.android.sdk.api.session.room.RoomDirectoryService import org.matrix.android.sdk.api.session.room.RoomDirectoryService
@ -75,6 +76,7 @@ interface Session :
TermsService, TermsService,
EventService, EventService,
ProfileService, ProfileService,
PresenceService,
PushRuleService, PushRuleService,
PushersService, PushersService,
SyncStatusService, SyncStatusService,

View file

@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.session.presence.model.PresenceContent
import timber.log.Timber import timber.log.Timber
typealias Content = JsonDict typealias Content = JsonDict
@ -305,3 +306,7 @@ fun Event.isReply(): Boolean {
fun Event.isEdition(): Boolean { fun Event.isEdition(): Boolean {
return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null
} }
fun Event.getPresenceContent(): PresenceContent? {
return content.toModel<PresenceContent>()
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.presence
import org.matrix.android.sdk.internal.session.presence.model.GetPresenceResponse
import org.matrix.android.sdk.internal.session.presence.model.PresenceEnum
/**
* This interface defines methods for handling user presence information.
*/
interface PresenceService {
/**
* Update the presence status for the current user
* @param presence the new presence state
* @param message the status message to attach to this state
*/
suspend fun setMyPresence(presence: PresenceEnum, message: String? = null)
/**
* Fetch the given user's presence state.
* @param userId the userId whose presence state to get.
*/
suspend fun fetchPresence(userId: String): GetPresenceResponse
}

View file

@ -16,12 +16,15 @@
package org.matrix.android.sdk.api.session.room.model package org.matrix.android.sdk.api.session.room.model
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
/** /**
* Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content
*/ */
data class RoomMemberSummary constructor( data class RoomMemberSummary constructor(
val membership: Membership, val membership: Membership,
val userId: String, val userId: String,
val userPresence: UserPresence? = null,
val displayName: String? = null, val displayName: String? = null,
val avatarUrl: String? = null val avatarUrl: String? = null
) )

View file

@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
/** /**
* This class holds some data of a room. * This class holds some data of a room.
@ -38,6 +39,7 @@ data class RoomSummary(
val joinRules: RoomJoinRules? = null, val joinRules: RoomJoinRules? = null,
val isDirect: Boolean = false, val isDirect: Boolean = false,
val directUserId: String? = null, val directUserId: String? = null,
val directUserPresence: UserPresence? = null,
val joinedMembersCount: Int? = 0, val joinedMembersCount: Int? = 0,
val invitedMembersCount: Int? = 0, val invitedMembersCount: Int? = 0,
val latestPreviewableEvent: TimelineEvent? = null, val latestPreviewableEvent: TimelineEvent? = null,

View file

@ -35,19 +35,21 @@ import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityField
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntityFields
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.query.process
import timber.log.Timber import timber.log.Timber
internal object RealmSessionStoreMigration : RealmMigration { internal object RealmSessionStoreMigration : RealmMigration {
const val SESSION_STORE_SCHEMA_VERSION = 17L const val SESSION_STORE_SCHEMA_VERSION = 18L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Session from $oldVersion to $newVersion") Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
@ -69,6 +71,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
if (oldVersion <= 14) migrateTo15(realm) if (oldVersion <= 14) migrateTo15(realm)
if (oldVersion <= 15) migrateTo16(realm) if (oldVersion <= 15) migrateTo16(realm)
if (oldVersion <= 16) migrateTo17(realm) if (oldVersion <= 16) migrateTo17(realm)
if (oldVersion <= 17) migrateTo18(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -338,4 +341,26 @@ internal object RealmSessionStoreMigration : RealmMigration {
realm.schema.get("EventInsertEntity") realm.schema.get("EventInsertEntity")
?.addField(EventInsertEntityFields.CAN_BE_PROCESSED, Boolean::class.java) ?.addField(EventInsertEntityFields.CAN_BE_PROCESSED, Boolean::class.java)
} }
private fun migrateTo18(realm: DynamicRealm) {
Timber.d("Step 17 -> 18")
realm.schema.create("UserPresenceEntity")
?.addField(UserPresenceEntityFields.USER_ID, String::class.java)
?.addPrimaryKey(UserPresenceEntityFields.USER_ID)
?.setRequired(UserPresenceEntityFields.USER_ID, true)
?.addField(UserPresenceEntityFields.PRESENCE_STR, String::class.java)
?.addField(UserPresenceEntityFields.LAST_ACTIVE_AGO, Long::class.java)
?.setNullable(UserPresenceEntityFields.LAST_ACTIVE_AGO, true)
?.addField(UserPresenceEntityFields.STATUS_MESSAGE, String::class.java)
?.addField(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, Boolean::class.java)
?.setNullable(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, true)
?.addField(UserPresenceEntityFields.AVATAR_URL, String::class.java)
val userPresenceEntity = realm.schema.get("UserPresenceEntity") ?: return
realm.schema.get("RoomSummaryEntity")
?.addRealmObjectField(RoomSummaryEntityFields.DIRECT_USER_PRESENCE.`$`, userPresenceEntity)
realm.schema.get("RoomMemberSummaryEntity")
?.addRealmObjectField(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`, userPresenceEntity)
}
} }

View file

@ -18,12 +18,14 @@ package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
internal object RoomMemberSummaryMapper { internal object RoomMemberSummaryMapper {
fun map(roomMemberSummaryEntity: RoomMemberSummaryEntity): RoomMemberSummary { fun map(roomMemberSummaryEntity: RoomMemberSummaryEntity): RoomMemberSummary {
return RoomMemberSummary( return RoomMemberSummary(
userId = roomMemberSummaryEntity.userId, userId = roomMemberSummaryEntity.userId,
userPresence = roomMemberSummaryEntity.userPresenceEntity?.toUserPresence(),
avatarUrl = roomMemberSummaryEntity.avatarUrl, avatarUrl = roomMemberSummaryEntity.avatarUrl,
displayName = roomMemberSummaryEntity.displayName, displayName = roomMemberSummaryEntity.displayName,
membership = roomMemberSummaryEntity.membership membership = roomMemberSummaryEntity.membership

View file

@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
import javax.inject.Inject import javax.inject.Inject
@ -48,6 +49,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
joinRules = roomSummaryEntity.joinRules, joinRules = roomSummaryEntity.joinRules,
isDirect = roomSummaryEntity.isDirect, isDirect = roomSummaryEntity.isDirect,
directUserId = roomSummaryEntity.directUserId, directUserId = roomSummaryEntity.directUserId,
directUserPresence = roomSummaryEntity.directUserPresence?.toUserPresence(),
latestPreviewableEvent = latestEvent, latestPreviewableEvent = latestEvent,
joinedMembersCount = roomSummaryEntity.joinedMembersCount, joinedMembersCount = roomSummaryEntity.joinedMembersCount,
invitedMembersCount = roomSummaryEntity.invitedMembersCount, invitedMembersCount = roomSummaryEntity.invitedMembersCount,

View file

@ -21,6 +21,7 @@ import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "", internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "",
@Index var userId: String = "", @Index var userId: String = "",
@ -40,6 +41,12 @@ internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String =
membershipStr = value.name membershipStr = value.name
} }
var userPresenceEntity: UserPresenceEntity? = null
set(value) {
if (value != field) field = value
}
fun getBestName() = displayName?.takeIf { it.isNotBlank() } ?: userId
fun toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
companion object companion object

View file

@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.VersioningState
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
internal open class RoomSummaryEntity( internal open class RoomSummaryEntity(
@PrimaryKey var roomId: String = "", @PrimaryKey var roomId: String = "",
@ -204,6 +205,11 @@ internal open class RoomSummaryEntity(
if (value != field) field = value if (value != field) field = value
} }
var directUserPresence: UserPresenceEntity? = null
set(value) {
if (value != field) field = value
}
var hasFailedSending: Boolean = false var hasFailedSending: Boolean = false
set(value) { set(value) {
if (value != field) field = value if (value != field) field = value

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.database.model package org.matrix.android.sdk.internal.database.model
import io.realm.annotations.RealmModule import io.realm.annotations.RealmModule
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
/** /**
* Realm module for Session * Realm module for Session
@ -64,6 +65,7 @@ import io.realm.annotations.RealmModule
WellknownIntegrationManagerConfigEntity::class, WellknownIntegrationManagerConfigEntity::class,
RoomAccountDataEntity::class, RoomAccountDataEntity::class,
SpaceChildSummaryEntity::class, SpaceChildSummaryEntity::class,
SpaceParentSummaryEntity::class SpaceParentSummaryEntity::class,
UserPresenceEntity::class
]) ])
internal class SessionRealmModule internal class SessionRealmModule

View file

@ -0,0 +1,49 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.model.presence
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.internal.session.presence.model.PresenceEnum
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
internal open class UserPresenceEntity(@PrimaryKey var userId: String = "",
var lastActiveAgo: Long? = null,
var statusMessage: String? = null,
var isCurrentlyActive: Boolean? = null,
var avatarUrl: String? = null
) : RealmObject() {
var presence: PresenceEnum
get() {
return PresenceEnum.valueOf(presenceStr)
}
set(value) {
presenceStr = value.name
}
private var presenceStr: String = PresenceEnum.UNAVAILABLE.name
companion object
}
internal fun UserPresenceEntity.toUserPresence() =
UserPresence(
lastActiveAgo,
statusMessage,
isCurrentlyActive,
presence)

View file

@ -21,6 +21,7 @@ import io.realm.RealmQuery
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery<RoomMemberSummaryEntity> { internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery<RoomMemberSummaryEntity> {
val query = realm val query = realm
@ -32,3 +33,13 @@ internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: Strin
} }
return query return query
} }
internal fun RoomMemberSummaryEntity.Companion.updateUserPresence(realm: Realm, userId: String, userPresenceEntity: UserPresenceEntity) {
realm.where<RoomMemberSummaryEntity>()
.equalTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.isNull(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`)
.findAll()
.map {
it.userPresenceEntity = userPresenceEntity
}
}

View file

@ -23,6 +23,7 @@ import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> { internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> {
val query = realm.where<RoomSummaryEntity>() val query = realm.where<RoomSummaryEntity>()
@ -67,3 +68,11 @@ internal fun RoomSummaryEntity.Companion.isDirect(realm: Realm, roomId: String):
.findAll() .findAll()
.isNotEmpty() .isNotEmpty()
} }
internal fun RoomSummaryEntity.Companion.updateDirectUserPresence(realm: Realm, directUserId: String, userPresenceEntity: UserPresenceEntity) {
RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.equalTo(RoomSummaryEntityFields.DIRECT_USER_ID, directUserId)
.findFirst()
?.directUserPresence = userPresenceEntity
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.query
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields
internal fun UserPresenceEntity.Companion.where(realm: Realm, userId: String): RealmQuery<UserPresenceEntity> {
return realm
.where<UserPresenceEntity>()
.equalTo(UserPresenceEntityFields.USER_ID, userId)
}

View file

@ -45,6 +45,7 @@ import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerS
import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.media.MediaService
import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.openid.OpenIdService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.presence.PresenceService
import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.pushers.PushersService import org.matrix.android.sdk.api.session.pushers.PushersService
import org.matrix.android.sdk.api.session.room.RoomDirectoryService import org.matrix.android.sdk.api.session.room.RoomDirectoryService
@ -127,6 +128,7 @@ internal class DefaultSession @Inject constructor(
private val callSignalingService: Lazy<CallSignalingService>, private val callSignalingService: Lazy<CallSignalingService>,
private val spaceService: Lazy<SpaceService>, private val spaceService: Lazy<SpaceService>,
private val openIdService: Lazy<OpenIdService>, private val openIdService: Lazy<OpenIdService>,
private val presenceService: Lazy<PresenceService>,
@UnauthenticatedWithCertificate @UnauthenticatedWithCertificate
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient> private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
) : Session, ) : Session,
@ -145,6 +147,7 @@ internal class DefaultSession @Inject constructor(
SecureStorageService by secureStorageService.get(), SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
ProfileService by profileService.get(), ProfileService by profileService.get(),
PresenceService by presenceService.get(),
AccountService by accountService.get() { AccountService by accountService.get() {
override val sharedSecretStorageService: SharedSecretStorageService override val sharedSecretStorageService: SharedSecretStorageService

View file

@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.session.identity.IdentityModule
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule
import org.matrix.android.sdk.internal.session.media.MediaModule import org.matrix.android.sdk.internal.session.media.MediaModule
import org.matrix.android.sdk.internal.session.openid.OpenIdModule import org.matrix.android.sdk.internal.session.openid.OpenIdModule
import org.matrix.android.sdk.internal.session.presence.di.PresenceModule
import org.matrix.android.sdk.internal.session.profile.ProfileModule import org.matrix.android.sdk.internal.session.profile.ProfileModule
import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker
import org.matrix.android.sdk.internal.session.pushers.PushersModule import org.matrix.android.sdk.internal.session.pushers.PushersModule
@ -94,7 +95,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
CallModule::class, CallModule::class,
SearchModule::class, SearchModule::class,
ThirdPartyModule::class, ThirdPartyModule::class,
SpaceModule::class SpaceModule::class,
PresenceModule::class
] ]
) )
@SessionScope @SessionScope

View file

@ -0,0 +1,43 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.presence
import org.matrix.android.sdk.internal.network.NetworkConstants
import org.matrix.android.sdk.internal.session.presence.model.GetPresenceResponse
import org.matrix.android.sdk.internal.session.presence.model.SetPresenceBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
import retrofit2.http.Path
internal interface PresenceAPI {
/**
* Set the presence status of the current user
* Ref: https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-presence-userid-status
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "presence/{userId}/status")
suspend fun setPresence(@Path("userId") userId: String,
@Body body: SetPresenceBody)
/**
* Get the given user's presence state.
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-presence-userid-status
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "presence/{userId}/status")
suspend fun getPresence(@Path("userId") userId: String): GetPresenceResponse
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.presence.di
import dagger.Binds
import dagger.Module
import dagger.Provides
import org.matrix.android.sdk.api.session.presence.PresenceService
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.presence.PresenceAPI
import org.matrix.android.sdk.internal.session.presence.service.DefaultPresenceService
import org.matrix.android.sdk.internal.session.presence.service.task.DefaultGetPresenceTask
import org.matrix.android.sdk.internal.session.presence.service.task.DefaultSetPresenceTask
import org.matrix.android.sdk.internal.session.presence.service.task.GetPresenceTask
import org.matrix.android.sdk.internal.session.presence.service.task.SetPresenceTask
import retrofit2.Retrofit
@Module
internal abstract class PresenceModule {
@Module
companion object {
@Provides
@JvmStatic
@SessionScope
fun providesPresenceAPI(retrofit: Retrofit): PresenceAPI {
return retrofit.create(PresenceAPI::class.java)
}
}
@Binds
abstract fun bindPresenceService(service: DefaultPresenceService): PresenceService
@Binds
abstract fun bindSetPresenceTask(task: DefaultSetPresenceTask): SetPresenceTask
@Binds
abstract fun bindGetPresenceTask(task: DefaultGetPresenceTask): GetPresenceTask
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.presence.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetPresenceResponse(
@Json(name = "presence")
val presence: PresenceEnum,
@Json(name = "last_active_ago")
val lastActiveAgo: Long? = null,
@Json(name = "status_msg")
val message: String? = null,
@Json(name = "currently_active")
val isCurrentlyActive: Boolean? = null
)

View file

@ -0,0 +1,32 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.presence.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Class representing the EventType.PRESENCE event content
*/
@JsonClass(generateAdapter = true)
data class PresenceContent(
@Json(name = "presence") val presence: PresenceEnum,
@Json(name = "last_active_ago") val lastActiveAgo: Long? = null,
@Json(name = "status_msg") val statusMessage: String? = null,
@Json(name = "currently_active") val isCurrentlyActive: Boolean = false,
@Json(name = "avatar_url") val avatarUrl: String? = null
)

View file

@ -0,0 +1,41 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.presence.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Annotate enums with @JsonClass(generateAdapter = false) to prevent
* them from being removed/obfuscated from your code by R8/ProGuard.
*/
@JsonClass(generateAdapter = false)
enum class PresenceEnum(val value: String) {
@Json(name = "online")
ONLINE("online"),
@Json(name = "offline")
OFFLINE("offline"),
@Json(name = "unavailable")
UNAVAILABLE("unavailable");
companion object {
fun from(s: String): PresenceEnum? = values().find { it.value == s }
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.presence.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class SetPresenceBody(
@Json(name = "presence")
val presence: PresenceEnum,
@Json(name = "status_msg")
val message: String?
)

View file

@ -0,0 +1,23 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.presence.model
data class UserPresence(
val lastActiveAgo: Long? = null,
val statusMessage: String? = null,
val isCurrentlyActive: Boolean? = null,
val presence: PresenceEnum = PresenceEnum.OFFLINE
)

View file

@ -0,0 +1,36 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.presence.service
import org.matrix.android.sdk.api.session.presence.PresenceService
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.presence.model.PresenceEnum
import org.matrix.android.sdk.internal.session.presence.service.task.GetPresenceTask
import org.matrix.android.sdk.internal.session.presence.service.task.SetPresenceTask
import javax.inject.Inject
internal class DefaultPresenceService @Inject constructor(@UserId private val userId: String,
private val setPresenceTask: SetPresenceTask,
private val getPresenceTask: GetPresenceTask) : PresenceService {
override suspend fun setMyPresence(presence: PresenceEnum, message: String?) {
setPresenceTask.execute(SetPresenceTask.Params(userId, presence, message))
}
override suspend fun fetchPresence(userId: String) = getPresenceTask.execute(GetPresenceTask.Params(userId))
}

View file

@ -0,0 +1,42 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.presence.service.task
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.presence.PresenceAPI
import org.matrix.android.sdk.internal.session.presence.model.GetPresenceResponse
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal abstract class GetPresenceTask : Task<GetPresenceTask.Params, GetPresenceResponse> {
data class Params(
val userId: String
)
}
internal class DefaultGetPresenceTask @Inject constructor(
private val presenceAPI: PresenceAPI,
private val globalErrorReceiver: GlobalErrorReceiver
) : GetPresenceTask() {
override suspend fun execute(params: Params): GetPresenceResponse {
return executeRequest(globalErrorReceiver) {
presenceAPI.getPresence(params.userId)
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.presence.service.task
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.presence.PresenceAPI
import org.matrix.android.sdk.internal.session.presence.model.PresenceEnum
import org.matrix.android.sdk.internal.session.presence.model.SetPresenceBody
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal abstract class SetPresenceTask : Task<SetPresenceTask.Params, Any> {
data class Params(
val userId: String,
val presence: PresenceEnum,
val message: String?
)
}
internal class DefaultSetPresenceTask @Inject constructor(
private val presenceAPI: PresenceAPI,
private val globalErrorReceiver: GlobalErrorReceiver
) : SetPresenceTask() {
override suspend fun execute(params: Params): Any {
return executeRequest(globalErrorReceiver) {
val setPresenceBody = SetPresenceBody(params.presence, params.message)
presenceAPI.setPresence(params.userId, setPresenceBody)
}
}
}

View file

@ -18,10 +18,11 @@ package org.matrix.android.sdk.internal.session.room.membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
internal object RoomMemberEntityFactory { internal object RoomMemberEntityFactory {
fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberSummaryEntity { fun create(roomId: String, userId: String, roomMember: RoomMemberContent, presence: UserPresenceEntity?): RoomMemberSummaryEntity {
val primaryKey = "${roomId}_$userId" val primaryKey = "${roomId}_$userId"
return RoomMemberSummaryEntity( return RoomMemberSummaryEntity(
primaryKey = primaryKey, primaryKey = primaryKey,
@ -31,6 +32,7 @@ internal object RoomMemberEntityFactory {
avatarUrl = roomMember.avatarUrl avatarUrl = roomMember.avatarUrl
).apply { ).apply {
membership = roomMember.membership membership = roomMember.membership
userPresenceEntity = presence
} }
} }
} }

View file

@ -20,6 +20,9 @@ import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
@ -47,7 +50,13 @@ internal class RoomMemberEventHandler @Inject constructor(
if (roomMember == null) { if (roomMember == null) {
return false return false
} }
val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) val roomMemberEntity = RoomMemberEntityFactory.create(
roomId,
userId,
roomMember,
// When an update is happening, insertOrUpdate replace existing values with null if they are not provided,
// but we want to preserve presence record value and not replace it with null
getExistingPresenceState(realm, roomId, userId))
realm.insertOrUpdate(roomMemberEntity) realm.insertOrUpdate(roomMemberEntity)
if (roomMember.membership.isActive()) { if (roomMember.membership.isActive()) {
val userEntity = UserEntityFactory.create(userId, roomMember) val userEntity = UserEntityFactory.create(userId, roomMember)
@ -60,7 +69,15 @@ internal class RoomMemberEventHandler @Inject constructor(
if (mxId != null && mxId != myUserId) { if (mxId != null && mxId != myUserId) {
aggregator?.directChatsToCheck?.put(roomId, mxId) aggregator?.directChatsToCheck?.put(roomId, mxId)
} }
return true return true
} }
/**
* Get the already existing presence state for a specific user & room in order NOT to be replaced in RoomMemberSummaryEntity
* by NULL value.
*/
private fun getExistingPresenceState(realm: Realm, roomId: String, userId: String): UserPresenceEntity? {
return RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()?.userPresenceEntity
}
} }

View file

@ -27,13 +27,13 @@ import org.matrix.android.sdk.internal.database.query.latestEvent
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler
import org.matrix.android.sdk.internal.session.sync.RoomFullyReadHandler
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.set import kotlin.collections.set

View file

@ -42,7 +42,7 @@ import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendState
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.Debouncer import org.matrix.android.sdk.internal.util.Debouncer

View file

@ -37,7 +37,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
internal class DefaultTimelineService @AssistedInject constructor( internal class DefaultTimelineService @AssistedInject constructor(

View file

@ -33,6 +33,12 @@ import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
import org.matrix.android.sdk.internal.session.initsync.reportSubtask import org.matrix.android.sdk.internal.session.initsync.reportSubtask
import org.matrix.android.sdk.internal.session.notification.ProcessEventForPushTask import org.matrix.android.sdk.internal.session.notification.ProcessEventForPushTask
import org.matrix.android.sdk.internal.session.sync.handler.CryptoSyncHandler
import org.matrix.android.sdk.internal.session.sync.handler.GroupSyncHandler
import org.matrix.android.sdk.internal.session.sync.handler.PresenceSyncHandler
import org.matrix.android.sdk.internal.session.sync.handler.SyncResponsePostTreatmentAggregatorHandler
import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.RoomSyncHandler
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber import timber.log.Timber
@ -55,7 +61,9 @@ internal class SyncResponseHandler @Inject constructor(
private val cryptoService: DefaultCryptoService, private val cryptoService: DefaultCryptoService,
private val tokenStore: SyncTokenStore, private val tokenStore: SyncTokenStore,
private val processEventForPushTask: ProcessEventForPushTask, private val processEventForPushTask: ProcessEventForPushTask,
private val pushRuleService: PushRuleService) { private val pushRuleService: PushRuleService,
private val presenceSyncHandler: PresenceSyncHandler
) {
suspend fun handleResponse(syncResponse: SyncResponse, suspend fun handleResponse(syncResponse: SyncResponse,
fromToken: String?, fromToken: String?,
@ -118,6 +126,13 @@ internal class SyncResponseHandler @Inject constructor(
}.also { }.also {
Timber.v("Finish handling accountData in $it ms") Timber.v("Finish handling accountData in $it ms")
} }
measureTimeMillis {
Timber.v("Handle Presence")
presenceSyncHandler.handle(realm,syncResponse.presence)
}.also {
Timber.v("Finish handling Presence in $it ms")
}
tokenStore.saveToken(realm, syncResponse.nextBatch) tokenStore.saveToken(realm, syncResponse.nextBatch)
} }

View file

@ -1,11 +1,11 @@
/* /*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C. * Copyright 2020 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync.handler
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync.handler
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.session.initsync.InitSyncStep import org.matrix.android.sdk.api.session.initsync.InitSyncStep

View file

@ -0,0 +1,60 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.sync.handler
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.getPresenceContent
import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence
import org.matrix.android.sdk.internal.database.query.updateUserPresence
import javax.inject.Inject
internal class PresenceSyncHandler @Inject constructor() {
fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) {
presenceSyncResponse?.events?.filter { event ->
event.type == EventType.PRESENCE
}?.forEach { event ->
val content = event.getPresenceContent() ?: return@forEach
val userId = event.senderId ?: return@forEach
val userPresenceEntity = UserPresenceEntity(
userId = userId,
lastActiveAgo = content.lastActiveAgo,
statusMessage = content.statusMessage,
isCurrentlyActive = content.isCurrentlyActive,
avatarUrl = content.avatarUrl
).also {
it.presence = content.presence
}
storePresenceToDB(realm, userPresenceEntity)
}
}
/**
* Store user presence to DB and update Direct Rooms and Room Member Summaries accordingly
*/
private fun storePresenceToDB(realm: Realm, userPresenceEntity: UserPresenceEntity) =
realm.copyToRealmOrUpdate(userPresenceEntity)?.apply {
RoomSummaryEntity.updateDirectUserPresence(realm, userPresenceEntity.userId, this)
RoomMemberSummaryEntity.updateUserPresence(realm, userPresenceEntity.userId, this)
}
}

View file

@ -1,11 +1,11 @@
/* /*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C. * Copyright 2020 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,9 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync.handler
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
import org.matrix.android.sdk.internal.session.sync.model.accountdata.toMutable import org.matrix.android.sdk.internal.session.sync.model.accountdata.toMutable
import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync.handler
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync.handler.room
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -23,6 +23,8 @@ import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
import org.matrix.android.sdk.internal.database.query.createUnmanaged import org.matrix.android.sdk.internal.database.query.createUnmanaged
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync.handler.room
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync.handler.room
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
@ -62,6 +62,9 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput
import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
import org.matrix.android.sdk.internal.session.sync.initialSyncStrategy
import org.matrix.android.sdk.internal.session.sync.parsing.RoomSyncAccountDataHandler import org.matrix.android.sdk.internal.session.sync.parsing.RoomSyncAccountDataHandler
import org.matrix.android.sdk.internal.util.computeBestChunkSize import org.matrix.android.sdk.internal.util.computeBestChunkSize
import timber.log.Timber import timber.log.Timber

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync.handler.room
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync.handler.room
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.sender.SenderInfo

View file

@ -28,8 +28,8 @@ import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityField
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.session.room.read.FullyReadContent import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
import org.matrix.android.sdk.internal.session.sync.RoomFullyReadHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler
import org.matrix.android.sdk.internal.session.sync.RoomTagHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler
import javax.inject.Inject import javax.inject.Inject
internal class RoomSyncAccountDataHandler @Inject constructor(private val roomTagHandler: RoomTagHandler, internal class RoomSyncAccountDataHandler @Inject constructor(private val roomTagHandler: RoomTagHandler,

View file

@ -19,13 +19,13 @@ package org.matrix.android.sdk.internal.session.user.accountdata
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent
import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataDataSource import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataDataSource
import org.matrix.android.sdk.internal.session.sync.UserAccountDataSyncHandler
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback

View file

@ -23,6 +23,7 @@ import android.widget.TextView
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.ui.views.PresenceStateImageView
import im.vector.app.core.ui.views.ShieldImageView import im.vector.app.core.ui.views.ShieldImageView
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item) @EpoxyModelClass(layout = R.layout.item_profile_matrix_item)
@ -31,6 +32,7 @@ abstract class ProfileMatrixItem : BaseProfileMatrixItem<ProfileMatrixItem.Holde
open class Holder : VectorEpoxyHolder() { open class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.matrixItemTitle) val titleView by bind<TextView>(R.id.matrixItemTitle)
val subtitleView by bind<TextView>(R.id.matrixItemSubtitle) val subtitleView by bind<TextView>(R.id.matrixItemSubtitle)
val presenceImageView by bind<PresenceStateImageView>(R.id.matrixItemPresenceImageView)
val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar) val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar)
val avatarDecorationImageView by bind<ShieldImageView>(R.id.matrixItemAvatarDecoration) val avatarDecorationImageView by bind<ShieldImageView>(R.id.matrixItemAvatarDecoration)
val editableView by bind<View>(R.id.matrixItemEditable) val editableView by bind<View>(R.id.matrixItemEditable)

View file

@ -0,0 +1,44 @@
/*
* Copyright 2021 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.core.epoxy.profiles
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item)
abstract class ProfileMatrixItemWithPresence : BaseProfileMatrixItem<ProfileMatrixItemWithPresence.Holder>() {
@EpoxyAttribute var powerLevelLabel: CharSequence? = null
@EpoxyAttribute var userPresence: UserPresence? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.powerLabel.setTextOrHide(powerLevelLabel)
holder.presenceImageView.render(userPresence = userPresence)
holder.editableView.isVisible = false
}
class Holder : ProfileMatrixItem.Holder() {
val powerLabel by bind<TextView>(R.id.matrixItemPowerLevelLabel)
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2021 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.core.ui.views
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import im.vector.app.R
import org.matrix.android.sdk.internal.session.presence.model.PresenceEnum
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
/**
* Custom ImageView to dynamically render Presence state in multiple screens
*/
class PresenceStateImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
fun render(showPresence: Boolean = true, userPresence: UserPresence?) {
isVisible = showPresence && userPresence != null
when (userPresence?.presence) {
PresenceEnum.ONLINE -> {
setImageResource(R.drawable.ic_presence_online)
contentDescription = context.getString(R.string.a11y_presence_online)
}
PresenceEnum.UNAVAILABLE -> {
setImageResource(R.drawable.ic_presence_offline)
contentDescription = context.getString(R.string.a11y_presence_unavailable)
}
PresenceEnum.OFFLINE -> {
setImageResource(R.drawable.ic_presence_offline)
contentDescription = context.getString(R.string.a11y_presence_offline)
}
}
}
}

View file

@ -1430,9 +1430,9 @@ class RoomDetailFragment @Inject constructor(
views.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN views.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN
views.roomToolbarTitleView.text = roomSummary.displayName views.roomToolbarTitleView.text = roomSummary.displayName
avatarRenderer.render(roomSummary.toMatrixItem(), views.roomToolbarAvatarImageView) avatarRenderer.render(roomSummary.toMatrixItem(), views.roomToolbarAvatarImageView)
renderSubTitle(typingMessage, roomSummary.topic) renderSubTitle(typingMessage, roomSummary.topic)
views.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel) views.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel)
views.roomToolbarPresenceImageView.render(roomSummary.isDirect, roomSummary.directUserPresence)
} }
} }

View file

@ -32,12 +32,14 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.ui.views.PresenceStateImageView
import im.vector.app.core.ui.views.ShieldImageView import im.vector.app.core.ui.views.ShieldImageView
import im.vector.app.features.displayname.getBestName import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
@EpoxyModelClass(layout = R.layout.item_room) @EpoxyModelClass(layout = R.layout.item_room)
abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() { abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
@ -53,6 +55,8 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var lastFormattedEvent: CharSequence @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var lastFormattedEvent: CharSequence
@EpoxyAttribute lateinit var lastEventTime: CharSequence @EpoxyAttribute lateinit var lastEventTime: CharSequence
@EpoxyAttribute var encryptionTrustLevel: RoomEncryptionTrustLevel? = null @EpoxyAttribute var encryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var userPresence: UserPresence? = null
@EpoxyAttribute var showPresence: Boolean = false
@EpoxyAttribute var izPublic: Boolean = false @EpoxyAttribute var izPublic: Boolean = false
@EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var hasUnreadMessage: Boolean = false @EpoxyAttribute var hasUnreadMessage: Boolean = false
@ -83,6 +87,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
renderSelection(holder, showSelected) renderSelection(holder, showSelected)
holder.typingView.setTextOrHide(typingMessage) holder.typingView.setTextOrHide(typingMessage)
holder.lastEventView.isInvisible = holder.typingView.isVisible holder.lastEventView.isInvisible = holder.typingView.isVisible
holder.roomAvatarPresenceImageView.render(showPresence, userPresence)
} }
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
@ -117,6 +122,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
val roomAvatarDecorationImageView by bind<ShieldImageView>(R.id.roomAvatarDecorationImageView) val roomAvatarDecorationImageView by bind<ShieldImageView>(R.id.roomAvatarDecorationImageView)
val roomAvatarPublicDecorationImageView by bind<ImageView>(R.id.roomAvatarPublicDecorationImageView) val roomAvatarPublicDecorationImageView by bind<ImageView>(R.id.roomAvatarPublicDecorationImageView)
val roomAvatarFailSendingImageView by bind<ImageView>(R.id.roomAvatarFailSendingImageView) val roomAvatarFailSendingImageView by bind<ImageView>(R.id.roomAvatarFailSendingImageView)
val roomAvatarPresenceImageView by bind<PresenceStateImageView>(R.id.roomAvatarPresenceImageView)
val rootView by bind<ViewGroup>(R.id.itemRoomLayout) val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
} }
} }

View file

@ -124,6 +124,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
// We do not display shield in the room list anymore // We do not display shield in the room list anymore
// .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel) // .encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
.izPublic(roomSummary.isPublic) .izPublic(roomSummary.isPublic)
.showPresence(roomSummary.isDirect)
.userPresence(roomSummary.directUserPresence)
.matrixItem(roomSummary.toMatrixItem()) .matrixItem(roomSummary.toMatrixItem())
.lastEventTime(latestEventTime) .lastEventTime(latestEventTime)
.typingMessage(typingMessage) .typingMessage(typingMessage)

View file

@ -219,6 +219,7 @@ class RoomProfileFragment @Inject constructor(
avatarRenderer.render(matrixItem, views.matrixProfileToolbarAvatarImageView) avatarRenderer.render(matrixItem, views.matrixProfileToolbarAvatarImageView)
headerViews.roomProfileDecorationImageView.render(it.roomEncryptionTrustLevel) headerViews.roomProfileDecorationImageView.render(it.roomEncryptionTrustLevel)
views.matrixProfileDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) views.matrixProfileDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel)
headerViews.roomProfilePresenceImageView.render(it.isDirect, it.directUserPresence)
} }
} }
roomProfileController.setData(state) roomProfileController.setData(state)

View file

@ -17,23 +17,29 @@
package im.vector.app.features.roomprofile.members package im.vector.app.features.roomprofile.members
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R
import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.epoxy.profiles.buildProfileSection import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.epoxy.profiles.profileMatrixItem import im.vector.app.core.epoxy.profiles.profileMatrixItem
import im.vector.app.core.epoxy.profiles.profileMatrixItemWithPresence
import im.vector.app.core.extensions.join import im.vector.app.core.extensions.join
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
import javax.inject.Inject import javax.inject.Inject
class RoomMemberListController @Inject constructor( class RoomMemberListController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val roomMemberSummaryFilter: RoomMemberSummaryFilter private val roomMemberSummaryFilter: RoomMemberSummaryFilter
) : TypedEpoxyController<RoomMemberListViewState>() { ) : TypedEpoxyController<RoomMemberListViewState>() {
@ -84,17 +90,10 @@ class RoomMemberListController @Inject constructor(
buildProfileSection( buildProfileSection(
stringProvider.getString(powerLevelCategory.titleRes) stringProvider.getString(powerLevelCategory.titleRes)
) )
filteredRoomMemberList.join( filteredRoomMemberList.join(
each = { _, roomMember -> each = { _, roomMember ->
profileMatrixItem { buildPresence(roomMember, powerLevelCategory, host, data, roomMember.userPresence)
id(roomMember.userId)
matrixItem(roomMember.toMatrixItem())
avatarRenderer(host.avatarRenderer)
userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId))
clickListener {
host.callback?.onRoomMemberClicked(roomMember)
}
}
}, },
between = { _, roomMemberBefore -> between = { _, roomMemberBefore ->
dividerItem { dividerItem {
@ -123,6 +122,33 @@ class RoomMemberListController @Inject constructor(
} }
} }
private fun buildPresence(roomMember: RoomMemberSummary,
powerLevelCategory: RoomMemberListCategories,
host: RoomMemberListController,
data: RoomMemberListViewState,
userPresence: UserPresence?
) {
val powerLabel = stringProvider.getString(powerLevelCategory.titleRes)
profileMatrixItemWithPresence {
id(roomMember.userId)
matrixItem(roomMember.toMatrixItem())
avatarRenderer(host.avatarRenderer)
userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId))
clickListener {
host.callback?.onRoomMemberClicked(roomMember)
}
userPresence(userPresence)
powerLevelLabel(
span {
span(powerLabel) {
textColor = host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
}
}
)
}
}
private fun buildThreePidInvites(data: RoomMemberListViewState) { private fun buildThreePidInvites(data: RoomMemberListViewState) {
val host = this val host = this
data.threePidInvites() data.threePidInvites()

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="11.89dp"
android:height="12dp"
android:viewportWidth="11.89"
android:viewportHeight="12"
>
<group>
<clip-path
android:pathData="M11.8857 6C11.8857 9.31371 9.225 12 5.94286 12C2.66071 12 0 9.31371 0 6C0 2.68629 2.66071 0 5.94286 0C9.225 0 11.8857 2.68629 11.8857 6Z"
/>
<path
android:pathData="M0 0V12H11.8857V0"
android:fillColor="?vctr_presence_indicator_offline"
/>
</group>
</vector>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="11.89dp"
android:height="12dp"
android:viewportWidth="11.89"
android:viewportHeight="12"
>
<group>
<clip-path
android:pathData="M11.8857 6C11.8857 9.31371 9.225 12 5.94286 12C2.66071 12 0 9.31371 0 6C0 2.68629 2.66071 0 5.94286 0C9.225 0 11.8857 2.68629 11.8857 6Z"
/>
<path
android:pathData="M0 0V12H11.8857V0"
android:fillColor="#0DBD8B"
/>
</group>
</vector>

View file

@ -45,19 +45,35 @@
<im.vector.app.core.ui.views.ShieldImageView <im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/roomToolbarDecorationImageView" android:id="@+id/roomToolbarDecorationImageView"
android:layout_width="20dp" android:layout_width="17dp"
android:layout_height="20dp" android:layout_height="17dp"
android:layout_marginStart="5dp"
android:layout_marginTop="2dp"
app:layout_constraintBottom_toBottomOf="@+id/roomToolbarTitleView"
app:layout_constraintStart_toEndOf="@+id/roomToolbarAvatarImageView"
app:layout_constraintTop_toTopOf="@+id/roomToolbarTitleView" />
<im.vector.app.core.ui.views.PresenceStateImageView
android:id="@+id/roomToolbarPresenceImageView"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/background_circle"
android:padding="2dp"
android:visibility="gone"
app:layout_constraintCircle="@+id/roomToolbarAvatarImageView" app:layout_constraintCircle="@+id/roomToolbarAvatarImageView"
app:layout_constraintCircleAngle="135" app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="20dp" app:layout_constraintCircleRadius="20dp"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints"
tools:src="@drawable/ic_presence_offline"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/roomToolbarTitleView" android:id="@+id/roomToolbarTitleView"
style="@style/Widget.Vector.TextView.HeadlineMedium" style="@style/Widget.Vector.TextView.HeadlineMedium"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="4dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
@ -65,8 +81,10 @@
app:layout_constraintBottom_toTopOf="@+id/roomToolbarSubtitleView" app:layout_constraintBottom_toTopOf="@+id/roomToolbarSubtitleView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/roomToolbarAvatarImageView" app:layout_constraintStart_toEndOf="@+id/roomToolbarDecorationImageView"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="7dp"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@sample/rooms.json/data/name" /> tools:text="@sample/rooms.json/data/name" />
<TextView <TextView
@ -74,9 +92,8 @@
style="@style/Widget.Vector.TextView.Body" style="@style/Widget.Vector.TextView.Body"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="7dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?vctr_content_primary" android:textColor="?vctr_content_primary"
@ -85,7 +102,8 @@
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/roomToolbarAvatarImageView" app:layout_constraintStart_toEndOf="@+id/roomToolbarAvatarImageView"
app:layout_constraintTop_toBottomOf="@+id/roomToolbarTitleView" app:layout_constraintTop_toBottomOf="@+id/roomToolbarTitleView"
tools:text="@sample/rooms.json/data/topic" /> tools:text="@sample/rooms.json/data/topic"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- https://tinyurl.com/PresenceListInRooms -->
<androidx.constraintlayout.widget.ConstraintLayout 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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@ -28,19 +29,35 @@
<im.vector.app.core.ui.views.ShieldImageView <im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/matrixItemAvatarDecoration" android:id="@+id/matrixItemAvatarDecoration"
android:layout_width="20dp" android:layout_width="16dp"
android:layout_height="20dp" android:layout_height="16dp"
android:layout_marginStart="5dp"
android:layout_marginTop="2dp"
app:layout_constraintBottom_toBottomOf="@+id/matrixItemTitle"
app:layout_constraintStart_toEndOf="@+id/matrixItemAvatar"
app:layout_constraintTop_toTopOf="@+id/matrixItemTitle" />
<im.vector.app.core.ui.views.PresenceStateImageView
android:id="@+id/matrixItemPresenceImageView"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/background_circle"
android:importantForAccessibility="no"
android:padding="2dp"
android:visibility="gone"
app:layout_constraintCircle="@+id/matrixItemAvatar" app:layout_constraintCircle="@+id/matrixItemAvatar"
app:layout_constraintCircleAngle="135" app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="16dp" app:layout_constraintCircleRadius="16dp"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints"
tools:src="@drawable/ic_presence_offline"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/matrixItemTitle" android:id="@+id/matrixItemTitle"
style="@style/Widget.Vector.TextView.Subtitle" style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="4dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
@ -48,9 +65,10 @@
app:layout_constrainedWidth="true" app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/matrixItemSubtitle" app:layout_constraintBottom_toTopOf="@+id/matrixItemSubtitle"
app:layout_constraintEnd_toStartOf="@+id/matrixItemPowerLevelLabel" app:layout_constraintEnd_toStartOf="@+id/matrixItemPowerLevelLabel"
app:layout_constraintStart_toEndOf="@id/matrixItemAvatar" app:layout_constraintStart_toEndOf="@+id/matrixItemAvatarDecoration"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginEnd="80dp" app:layout_goneMarginEnd="80dp"
app:layout_goneMarginStart="7dp"
tools:text="@sample/users.json/data/displayName" /> tools:text="@sample/users.json/data/displayName" />
<TextView <TextView
@ -58,7 +76,7 @@
style="@style/Widget.Vector.TextView.Body" style="@style/Widget.Vector.TextView.Body"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="7dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"

View file

@ -84,6 +84,21 @@
app:layout_constraintCircleAngle="135" app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="28dp" app:layout_constraintCircleRadius="28dp"
tools:ignore="MissingConstraints" tools:ignore="MissingConstraints"
tools:visibility="gone" />
<im.vector.app.core.ui.views.PresenceStateImageView
android:id="@+id/roomAvatarPresenceImageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:background="@drawable/background_circle"
android:importantForAccessibility="no"
android:padding="2dp"
android:visibility="gone"
app:layout_constraintCircle="@id/roomAvatarContainer"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="28dp"
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_presence_offline"
tools:visibility="visible" /> tools:visibility="visible" />
<!-- Margin bottom does not work, so I use space --> <!-- Margin bottom does not work, so I use space -->

View file

@ -30,14 +30,33 @@
app:layout_constraintVertical_chainStyle="spread_inside" app:layout_constraintVertical_chainStyle="spread_inside"
tools:src="@sample/user_round_avatars" /> tools:src="@sample/user_round_avatars" />
<im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/memberProfileDecorationImageView" <im.vector.app.core.ui.views.PresenceStateImageView
android:layout_width="48dp" android:id="@+id/memberProfilePresenceImageView"
android:layout_height="48dp" android:layout_width="26dp"
android:layout_height="26dp"
android:background="@drawable/background_circle"
android:importantForAccessibility="no"
android:padding="3dp"
android:visibility="gone"
app:layout_constraintCircle="@+id/memberProfileAvatarView" app:layout_constraintCircle="@+id/memberProfileAvatarView"
app:layout_constraintCircleAngle="135" app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="64dp" app:layout_constraintCircleRadius="64dp"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints"
tools:src="@drawable/ic_presence_offline"
tools:visibility="visible" />
<im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/memberProfileDecorationImageView"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@+id/memberProfileNameView"
app:layout_constraintEnd_toStartOf="@+id/memberProfileNameView"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/memberProfileNameView"/>
<TextView <TextView
android:id="@+id/memberProfileNameView" android:id="@+id/memberProfileNameView"
@ -48,7 +67,7 @@
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/memberProfileIdView" app:layout_constraintBottom_toTopOf="@+id/memberProfileIdView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toEndOf="@+id/memberProfileDecorationImageView"
app:layout_constraintTop_toBottomOf="@+id/memberProfileAvatarView" app:layout_constraintTop_toBottomOf="@+id/memberProfileAvatarView"
tools:text="@sample/users.json/data/displayName" /> tools:text="@sample/users.json/data/displayName" />

View file

@ -20,25 +20,43 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@sample/room_round_avatars" /> tools:src="@sample/room_round_avatars" />
<im.vector.app.core.ui.views.ShieldImageView <im.vector.app.core.ui.views.PresenceStateImageView
android:id="@+id/roomProfileDecorationImageView" android:id="@+id/roomProfilePresenceImageView"
android:layout_width="48dp" android:layout_width="26dp"
android:layout_height="48dp" android:layout_height="26dp"
android:background="@drawable/background_circle"
android:importantForAccessibility="no"
android:padding="3dp"
android:visibility="gone"
app:layout_constraintCircle="@+id/roomProfileAvatarView" app:layout_constraintCircle="@+id/roomProfileAvatarView"
app:layout_constraintCircleAngle="135" app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="64dp" app:layout_constraintCircleRadius="64dp"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints"
tools:src="@drawable/ic_presence_offline"
tools:visibility="visible" />
<im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/roomProfileDecorationImageView"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@+id/roomProfileNameView"
app:layout_constraintEnd_toStartOf="@+id/roomProfileNameView"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/roomProfileNameView"/>
<TextView <TextView
android:id="@+id/roomProfileNameView" android:id="@+id/roomProfileNameView"
style="@style/Widget.Vector.TextView.Title" style="@style/Widget.Vector.TextView.Title"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/roomProfileAliasView" app:layout_constraintBottom_toTopOf="@+id/roomProfileAliasView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toEndOf="@+id/roomProfileDecorationImageView"
app:layout_constraintTop_toBottomOf="@+id/roomProfileAvatarView" app:layout_constraintTop_toBottomOf="@+id/roomProfileAvatarView"
tools:text="@sample/rooms.json/data/name" /> tools:text="@sample/rooms.json/data/name" />

View file

@ -3404,6 +3404,9 @@
<string name="a11y_view_read_receipts">View read receipts</string> <string name="a11y_view_read_receipts">View read receipts</string>
<string name="a11y_public_room">Public room</string> <string name="a11y_public_room">Public room</string>
<string name="a11y_public_space">Public space</string> <string name="a11y_public_space">Public space</string>
<string name="a11y_presence_online">Online</string>
<string name="a11y_presence_offline">Offline</string>
<string name="a11y_presence_unavailable">Unavailable</string>
<string name="dev_tools_menu_name">Dev Tools</string> <string name="dev_tools_menu_name">Dev Tools</string>
<string name="dev_tools_explore_room_state">Explore Room State</string> <string name="dev_tools_explore_room_state">Explore Room State</string>