Merge pull request from vector-im/feature/aris/presence

Feature/aris/presence
This commit is contained in:
Benoit Marty 2021-10-13 09:58:22 +02:00 committed by GitHub
commit 085da6c99a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 1083 additions and 61 deletions
changelog.d
gradle.properties
library/ui-styles/src/main/res/values
matrix-sdk-android/src/main/java/org/matrix/android/sdk
vector/src/main

1
changelog.d/4090.feature Normal file
View file

@ -0,0 +1 @@
Handle Presence support, for Direct Message room

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

@ -43,6 +43,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
@ -76,6 +77,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.api.session.presence.model.PresenceEnum
import org.matrix.android.sdk.api.session.presence.model.UserPresence
/**
* 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 statusMsg the status message to attach to this state
*/
suspend fun setMyPresence(presence: PresenceEnum, statusMsg: String? = null)
/**
* Fetch the given user's presence state.
* @param userId the userId whose presence state to get.
*/
suspend fun fetchPresence(userId: String): UserPresence
// TODO Add live data (of Flow) of the presence of a userId
}

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.api.session.presence.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@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,24 @@
/*
* 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.model
data class UserPresence(
val lastActiveAgo: Long? = null,
val statusMessage: String? = null,
val isCurrentlyActive: Boolean? = null,
val presence: PresenceEnum = PresenceEnum.OFFLINE
)

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.api.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

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.room.model package org.matrix.android.sdk.api.session.room.model
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.presence.model.UserPresence
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.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
@ -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,27 @@ 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)
?.addField(UserPresenceEntityFields.DISPLAY_NAME, 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,11 @@ 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 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,51 @@
/*
* 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.api.session.presence.model.PresenceEnum
import org.matrix.android.sdk.api.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,
var displayName: 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

@ -46,6 +46,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
@ -129,6 +130,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,
@ -147,6 +149,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

@ -44,6 +44,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
@ -96,6 +97,7 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
SearchModule::class, SearchModule::class,
ThirdPartyModule::class, ThirdPartyModule::class,
SpaceModule::class, SpaceModule::class,
PresenceModule::class,
RequestModule::class RequestModule::class
] ]
) )

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,33 @@
/*
* 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
import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
@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,52 @@
/*
* 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
import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
/**
* Class representing the EventType.PRESENCE event content
*/
@JsonClass(generateAdapter = true)
data class PresenceContent(
/**
* Required. The presence state for this user. One of: ["online", "offline", "unavailable"]
*/
@Json(name = "presence") val presence: PresenceEnum,
/**
* The last time since this used performed some action, in milliseconds.
*/
@Json(name = "last_active_ago") val lastActiveAgo: Long? = null,
/**
* An optional description to accompany the presence.
*/
@Json(name = "status_msg") val statusMessage: String? = null,
/**
* Whether the user is currently active
*/
@Json(name = "currently_active") val isCurrentlyActive: Boolean = false,
/**
* The current avatar URL for this user, if any.
*/
@Json(name = "avatar_url") val avatarUrl: String? = null,
/**
* The current display name for this user, if any.
*/
@Json(name = "displayname") val displayName: String? = null
)

View file

@ -0,0 +1,28 @@
/*
* 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
import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
@JsonClass(generateAdapter = true)
internal data class SetPresenceBody(
@Json(name = "presence")
val presence: PresenceEnum,
@Json(name = "status_msg")
val statusMsg: String?
)

View file

@ -0,0 +1,48 @@
/*
* 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.api.session.presence.model.PresenceEnum
import org.matrix.android.sdk.api.session.presence.model.UserPresence
import org.matrix.android.sdk.internal.di.UserId
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, statusMsg: String?) {
setPresenceTask.execute(SetPresenceTask.Params(userId, presence, statusMsg))
}
override suspend fun fetchPresence(userId: String): UserPresence {
val result = getPresenceTask.execute(GetPresenceTask.Params(userId))
return UserPresence(
lastActiveAgo = result.lastActiveAgo,
statusMessage = result.message,
isCurrentlyActive = result.isCurrentlyActive,
presence = result.presence
)
}
}

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.api.session.presence.model.PresenceEnum
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.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 statusMsg: 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.statusMsg)
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

@ -30,8 +30,8 @@ import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver 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.ReadReceiptHandler
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.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 timber.log.Timber import timber.log.Timber

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)
} }
@ -145,7 +160,8 @@ internal class SyncResponseHandler @Inject constructor(
private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) { private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) {
roomsSyncResponse.invite.keys.forEach { roomId -> roomsSyncResponse.invite.keys.forEach { roomId ->
sessionListeners.dispatch { session, listener -> sessionListeners.dispatch { session, listener ->
listener.onNewInvitedRoom(session, roomId) } listener.onNewInvitedRoom(session, roomId)
}
} }
} }

View file

@ -5,7 +5,7 @@
* 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,61 @@
/*
* 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,
displayName = content.displayName
).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

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

@ -25,7 +25,7 @@ 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.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.session.sync.handler.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,34 @@
/*
* 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 com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import org.matrix.android.sdk.api.session.presence.model.UserPresence
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item)
abstract class ProfileMatrixItemWithPowerLevelWithPresence : ProfileMatrixItemWithPowerLevel() {
@EpoxyAttribute var userPresence: UserPresence? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.presenceImageView.render(userPresence = userPresence)
}
}

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.api.session.presence.model.PresenceEnum
import org.matrix.android.sdk.api.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

@ -1427,9 +1427,10 @@ 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)
views.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect
} }
} }

View file

@ -32,11 +32,13 @@ 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.session.presence.model.UserPresence
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_room) @EpoxyModelClass(layout = R.layout.item_room)
@ -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,8 @@ 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)
headerViews.roomProfilePublicImageView.isVisible = it.isPublic && !it.isDirect
} }
} }
roomProfileController.setData(state) roomProfileController.setData(state)

View file

@ -17,14 +17,19 @@
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.profileMatrixItemWithPowerLevelWithPresence
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.presence.model.UserPresence
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
@ -34,6 +39,7 @@ 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)
profileMatrixItemWithPowerLevelWithPresence {
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,50 @@
<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:layout_constraintCircleRadius="8dp"
tools:src="@drawable/ic_presence_offline"
tools:visibility="visible" />
<ImageView
android:id="@+id/roomToolbarPublicImageView"
android:layout_width="13dp"
android:layout_height="13dp"
android:background="@drawable/background_circle"
android:contentDescription="@string/a11y_public_room"
android:padding="1dp"
android:src="@drawable/ic_public_room"
android:visibility="gone"
app:layout_constraintCircle="@+id/roomToolbarAvatarImageView"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="20dp"
tools:ignore="MissingConstraints"
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 +96,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_constraintVertical_chainStyle="packed"
app:layout_goneMarginStart="7dp"
tools:text="@sample/rooms.json/data/name" /> tools:text="@sample/rooms.json/data/name" />
<TextView <TextView
@ -74,9 +107,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 +117,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

@ -86,6 +86,22 @@
tools:ignore="MissingConstraints" tools:ignore="MissingConstraints"
tools:visibility="visible" /> tools:visibility="visible" />
<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:layout_constraintCircleRadius="8dp"
tools:src="@drawable/ic_presence_offline"
tools:visibility="visible" />
<!-- Margin bottom does not work, so I use space --> <!-- Margin bottom does not work, so I use space -->
<Space <Space
android:id="@+id/roomAvatarBottomSpace" android:id="@+id/roomAvatarBottomSpace"

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,58 @@
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" />
<ImageView
android:id="@+id/roomProfilePublicImageView"
android:layout_width="28dp"
android:layout_height="28dp"
android:background="@drawable/background_circle"
android:contentDescription="@string/a11y_public_room"
android:padding="2dp"
android:src="@drawable/ic_public_room"
android:visibility="gone"
app:layout_constraintCircle="@+id/roomProfileAvatarView"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="64dp"
tools:ignore="MissingConstraints"
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

@ -3406,6 +3406,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>