* Implement Presence Service:

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

View file

@ -23,3 +23,6 @@ vector.debugPrivateData=false
# httpLogLevel values: NONE, BASIC, HEADERS, BODY
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_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>

View file

@ -42,6 +42,9 @@
<item name="vctr_markdown_block_background_color">@android:color/black</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 -->
<item name="vctr_header_background">?vctr_system</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_spoiler_background_color">#FF000000</item>
<!-- Presence Indicator colors -->
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_light</item>
<!-- Some alias -->
<item name="vctr_header_background">?vctr_system</item>
<item name="vctr_list_separator">?vctr_content_quinary</item>

View file

@ -42,6 +42,7 @@ import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerS
import org.matrix.android.sdk.api.session.media.MediaService
import org.matrix.android.sdk.api.session.openid.OpenIdService
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.pushers.PushersService
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
@ -75,6 +76,7 @@ interface Session :
TermsService,
EventService,
ProfileService,
PresenceService,
PushRuleService,
PushersService,
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.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.session.presence.model.PresenceContent
import timber.log.Timber
typealias Content = JsonDict
@ -305,3 +306,7 @@ fun Event.isReply(): Boolean {
fun Event.isEdition(): Boolean {
return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null
}
fun Event.getPresenceContent(): PresenceContent? {
return content.toModel<PresenceContent>()
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.matrix.android.sdk.api.session.presence
import org.matrix.android.sdk.internal.session.presence.model.GetPresenceResponse
import org.matrix.android.sdk.internal.session.presence.model.PresenceEnum
/**
* This interface defines methods for handling user presence information.
*/
interface PresenceService {
/**
* Update the presence status for the current user
* @param presence the new presence state
* @param message the status message to attach to this state
*/
suspend fun setMyPresence(presence: PresenceEnum, message: String? = null)
/**
* Fetch the given user's presence state.
* @param userId the userId whose presence state to get.
*/
suspend fun fetchPresence(userId: String): GetPresenceResponse
}

View file

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

View file

@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
/**
* This class holds some data of a room.
@ -38,6 +39,7 @@ data class RoomSummary(
val joinRules: RoomJoinRules? = null,
val isDirect: Boolean = false,
val directUserId: String? = null,
val directUserPresence: UserPresence? = null,
val joinedMembersCount: Int? = 0,
val invitedMembersCount: Int? = 0,
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.RoomAccountDataEntityFields
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.RoomSummaryEntityFields
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.SpaceParentSummaryEntityFields
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.query.process
import timber.log.Timber
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) {
Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
@ -69,6 +71,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
if (oldVersion <= 14) migrateTo15(realm)
if (oldVersion <= 15) migrateTo16(realm)
if (oldVersion <= 16) migrateTo17(realm)
if (oldVersion <= 17) migrateTo18(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -338,4 +341,26 @@ internal object RealmSessionStoreMigration : RealmMigration {
realm.schema.get("EventInsertEntity")
?.addField(EventInsertEntityFields.CAN_BE_PROCESSED, Boolean::class.java)
}
private fun migrateTo18(realm: DynamicRealm) {
Timber.d("Step 17 -> 18")
realm.schema.create("UserPresenceEntity")
?.addField(UserPresenceEntityFields.USER_ID, String::class.java)
?.addPrimaryKey(UserPresenceEntityFields.USER_ID)
?.setRequired(UserPresenceEntityFields.USER_ID, true)
?.addField(UserPresenceEntityFields.PRESENCE_STR, String::class.java)
?.addField(UserPresenceEntityFields.LAST_ACTIVE_AGO, Long::class.java)
?.setNullable(UserPresenceEntityFields.LAST_ACTIVE_AGO, true)
?.addField(UserPresenceEntityFields.STATUS_MESSAGE, String::class.java)
?.addField(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, Boolean::class.java)
?.setNullable(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, true)
?.addField(UserPresenceEntityFields.AVATAR_URL, String::class.java)
val userPresenceEntity = realm.schema.get("UserPresenceEntity") ?: return
realm.schema.get("RoomSummaryEntity")
?.addRealmObjectField(RoomSummaryEntityFields.DIRECT_USER_PRESENCE.`$`, userPresenceEntity)
realm.schema.get("RoomMemberSummaryEntity")
?.addRealmObjectField(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`, userPresenceEntity)
}
}

View file

@ -18,12 +18,14 @@ package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
internal object RoomMemberSummaryMapper {
fun map(roomMemberSummaryEntity: RoomMemberSummaryEntity): RoomMemberSummary {
return RoomMemberSummary(
userId = roomMemberSummaryEntity.userId,
userPresence = roomMemberSummaryEntity.userPresenceEntity?.toUserPresence(),
avatarUrl = roomMemberSummaryEntity.avatarUrl,
displayName = roomMemberSummaryEntity.displayName,
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.tag.RoomTag
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 javax.inject.Inject
@ -48,6 +49,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
joinRules = roomSummaryEntity.joinRules,
isDirect = roomSummaryEntity.isDirect,
directUserId = roomSummaryEntity.directUserId,
directUserPresence = roomSummaryEntity.directUserPresence?.toUserPresence(),
latestPreviewableEvent = latestEvent,
joinedMembersCount = roomSummaryEntity.joinedMembersCount,
invitedMembersCount = roomSummaryEntity.invitedMembersCount,

View file

@ -21,6 +21,7 @@ import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.api.session.room.model.Membership
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 = "",
@Index var userId: String = "",
@ -40,6 +41,12 @@ internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String =
membershipStr = value.name
}
var userPresenceEntity: UserPresenceEntity? = null
set(value) {
if (value != field) field = value
}
fun getBestName() = displayName?.takeIf { it.isNotBlank() } ?: userId
fun toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
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.VersioningState
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(
@PrimaryKey var roomId: String = "",
@ -204,6 +205,11 @@ internal open class RoomSummaryEntity(
if (value != field) field = value
}
var directUserPresence: UserPresenceEntity? = null
set(value) {
if (value != field) field = value
}
var hasFailedSending: Boolean = false
set(value) {
if (value != field) field = value

View file

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

View file

@ -0,0 +1,49 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.model.presence
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.internal.session.presence.model.PresenceEnum
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
internal open class UserPresenceEntity(@PrimaryKey var userId: String = "",
var lastActiveAgo: Long? = null,
var statusMessage: String? = null,
var isCurrentlyActive: Boolean? = null,
var avatarUrl: String? = null
) : RealmObject() {
var presence: PresenceEnum
get() {
return PresenceEnum.valueOf(presenceStr)
}
set(value) {
presenceStr = value.name
}
private var presenceStr: String = PresenceEnum.UNAVAILABLE.name
companion object
}
internal fun UserPresenceEntity.toUserPresence() =
UserPresence(
lastActiveAgo,
statusMessage,
isCurrentlyActive,
presence)

View file

@ -21,6 +21,7 @@ import io.realm.RealmQuery
import io.realm.kotlin.where
import 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.presence.UserPresenceEntity
internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery<RoomMemberSummaryEntity> {
val query = realm
@ -32,3 +33,13 @@ internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: Strin
}
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 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.presence.UserPresenceEntity
internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> {
val query = realm.where<RoomSummaryEntity>()
@ -67,3 +68,11 @@ internal fun RoomSummaryEntity.Companion.isDirect(realm: Realm, roomId: String):
.findAll()
.isNotEmpty()
}
internal fun RoomSummaryEntity.Companion.updateDirectUserPresence(realm: Realm, directUserId: String, userPresenceEntity: UserPresenceEntity) {
RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.equalTo(RoomSummaryEntityFields.DIRECT_USER_ID, directUserId)
.findFirst()
?.directUserPresence = userPresenceEntity
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.query
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields
internal fun UserPresenceEntity.Companion.where(realm: Realm, userId: String): RealmQuery<UserPresenceEntity> {
return realm
.where<UserPresenceEntity>()
.equalTo(UserPresenceEntityFields.USER_ID, userId)
}

View file

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

View file

@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.session.identity.IdentityModule
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule
import org.matrix.android.sdk.internal.session.media.MediaModule
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.pushers.AddPusherWorker
import org.matrix.android.sdk.internal.session.pushers.PushersModule
@ -94,7 +95,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
CallModule::class,
SearchModule::class,
ThirdPartyModule::class,
SpaceModule::class
SpaceModule::class,
PresenceModule::class
]
)
@SessionScope

View file

@ -0,0 +1,43 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.presence
import org.matrix.android.sdk.internal.network.NetworkConstants
import org.matrix.android.sdk.internal.session.presence.model.GetPresenceResponse
import org.matrix.android.sdk.internal.session.presence.model.SetPresenceBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
import retrofit2.http.Path
internal interface PresenceAPI {
/**
* Set the presence status of the current user
* Ref: https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-presence-userid-status
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "presence/{userId}/status")
suspend fun setPresence(@Path("userId") userId: String,
@Body body: SetPresenceBody)
/**
* Get the given user's presence state.
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-presence-userid-status
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "presence/{userId}/status")
suspend fun getPresence(@Path("userId") userId: String): GetPresenceResponse
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.presence.di
import dagger.Binds
import dagger.Module
import dagger.Provides
import org.matrix.android.sdk.api.session.presence.PresenceService
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.presence.PresenceAPI
import org.matrix.android.sdk.internal.session.presence.service.DefaultPresenceService
import org.matrix.android.sdk.internal.session.presence.service.task.DefaultGetPresenceTask
import org.matrix.android.sdk.internal.session.presence.service.task.DefaultSetPresenceTask
import org.matrix.android.sdk.internal.session.presence.service.task.GetPresenceTask
import org.matrix.android.sdk.internal.session.presence.service.task.SetPresenceTask
import retrofit2.Retrofit
@Module
internal abstract class PresenceModule {
@Module
companion object {
@Provides
@JvmStatic
@SessionScope
fun providesPresenceAPI(retrofit: Retrofit): PresenceAPI {
return retrofit.create(PresenceAPI::class.java)
}
}
@Binds
abstract fun bindPresenceService(service: DefaultPresenceService): PresenceService
@Binds
abstract fun bindSetPresenceTask(task: DefaultSetPresenceTask): SetPresenceTask
@Binds
abstract fun bindGetPresenceTask(task: DefaultGetPresenceTask): GetPresenceTask
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.presence.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetPresenceResponse(
@Json(name = "presence")
val presence: PresenceEnum,
@Json(name = "last_active_ago")
val lastActiveAgo: Long? = null,
@Json(name = "status_msg")
val message: String? = null,
@Json(name = "currently_active")
val isCurrentlyActive: Boolean? = null
)

View file

@ -0,0 +1,32 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.presence.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Class representing the EventType.PRESENCE event content
*/
@JsonClass(generateAdapter = true)
data class PresenceContent(
@Json(name = "presence") val presence: PresenceEnum,
@Json(name = "last_active_ago") val lastActiveAgo: Long? = null,
@Json(name = "status_msg") val statusMessage: String? = null,
@Json(name = "currently_active") val isCurrentlyActive: Boolean = false,
@Json(name = "avatar_url") val avatarUrl: String? = null
)

View file

@ -0,0 +1,41 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.presence.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Annotate enums with @JsonClass(generateAdapter = false) to prevent
* them from being removed/obfuscated from your code by R8/ProGuard.
*/
@JsonClass(generateAdapter = false)
enum class PresenceEnum(val value: String) {
@Json(name = "online")
ONLINE("online"),
@Json(name = "offline")
OFFLINE("offline"),
@Json(name = "unavailable")
UNAVAILABLE("unavailable");
companion object {
fun from(s: String): PresenceEnum? = values().find { it.value == s }
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.presence.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class SetPresenceBody(
@Json(name = "presence")
val presence: PresenceEnum,
@Json(name = "status_msg")
val message: String?
)

View file

@ -0,0 +1,23 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.presence.model
data class UserPresence(
val lastActiveAgo: Long? = null,
val statusMessage: String? = null,
val isCurrentlyActive: Boolean? = null,
val presence: PresenceEnum = PresenceEnum.OFFLINE
)

View file

@ -0,0 +1,36 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.matrix.android.sdk.internal.session.presence.service
import org.matrix.android.sdk.api.session.presence.PresenceService
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.presence.model.PresenceEnum
import org.matrix.android.sdk.internal.session.presence.service.task.GetPresenceTask
import org.matrix.android.sdk.internal.session.presence.service.task.SetPresenceTask
import javax.inject.Inject
internal class DefaultPresenceService @Inject constructor(@UserId private val userId: String,
private val setPresenceTask: SetPresenceTask,
private val getPresenceTask: GetPresenceTask) : PresenceService {
override suspend fun setMyPresence(presence: PresenceEnum, message: String?) {
setPresenceTask.execute(SetPresenceTask.Params(userId, presence, message))
}
override suspend fun fetchPresence(userId: String) = getPresenceTask.execute(GetPresenceTask.Params(userId))
}

View file

@ -0,0 +1,42 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.matrix.android.sdk.internal.session.presence.service.task
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.presence.PresenceAPI
import org.matrix.android.sdk.internal.session.presence.model.GetPresenceResponse
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal abstract class GetPresenceTask : Task<GetPresenceTask.Params, GetPresenceResponse> {
data class Params(
val userId: String
)
}
internal class DefaultGetPresenceTask @Inject constructor(
private val presenceAPI: PresenceAPI,
private val globalErrorReceiver: GlobalErrorReceiver
) : GetPresenceTask() {
override suspend fun execute(params: Params): GetPresenceResponse {
return executeRequest(globalErrorReceiver) {
presenceAPI.getPresence(params.userId)
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.matrix.android.sdk.internal.session.presence.service.task
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.presence.PresenceAPI
import org.matrix.android.sdk.internal.session.presence.model.PresenceEnum
import org.matrix.android.sdk.internal.session.presence.model.SetPresenceBody
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal abstract class SetPresenceTask : Task<SetPresenceTask.Params, Any> {
data class Params(
val userId: String,
val presence: PresenceEnum,
val message: String?
)
}
internal class DefaultSetPresenceTask @Inject constructor(
private val presenceAPI: PresenceAPI,
private val globalErrorReceiver: GlobalErrorReceiver
) : SetPresenceTask() {
override suspend fun execute(params: Params): Any {
return executeRequest(globalErrorReceiver) {
val setPresenceBody = SetPresenceBody(params.presence, params.message)
presenceAPI.setPresence(params.userId, setPresenceBody)
}
}
}

View file

@ -18,10 +18,11 @@ package org.matrix.android.sdk.internal.session.room.membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
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"
return RoomMemberSummaryEntity(
primaryKey = primaryKey,
@ -31,6 +32,7 @@ internal object RoomMemberEntityFactory {
avatarUrl = roomMember.avatarUrl
).apply {
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.EventType
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.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
@ -47,7 +50,13 @@ internal class RoomMemberEventHandler @Inject constructor(
if (roomMember == null) {
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)
if (roomMember.membership.isActive()) {
val userEntity = UserEntityFactory.create(userId, roomMember)
@ -60,7 +69,15 @@ internal class RoomMemberEventHandler @Inject constructor(
if (mxId != null && mxId != myUserId) {
aggregator?.directChatsToCheck?.put(roomId, mxId)
}
return true
}
/**
* Get the already existing presence state for a specific user & room in order NOT to be replaced in RoomMemberSummaryEntity
* by NULL value.
*/
private fun getExistingPresenceState(realm: Realm, roomId: String, userId: String): UserPresenceEntity? {
return RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()?.userPresenceEntity
}
}

View file

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

View file

@ -42,7 +42,7 @@ import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendState
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
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.configureWith
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.di.SessionDatabase
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
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.reportSubtask
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.worker.WorkerParamsFactory
import timber.log.Timber
@ -55,7 +61,9 @@ internal class SyncResponseHandler @Inject constructor(
private val cryptoService: DefaultCryptoService,
private val tokenStore: SyncTokenStore,
private val processEventForPushTask: ProcessEventForPushTask,
private val pushRuleService: PushRuleService) {
private val pushRuleService: PushRuleService,
private val presenceSyncHandler: PresenceSyncHandler
) {
suspend fun handleResponse(syncResponse: SyncResponse,
fromToken: String?,
@ -118,6 +126,13 @@ internal class SyncResponseHandler @Inject constructor(
}.also {
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)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View file

@ -14,7 +14,7 @@
* 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.events.model.Event

View file

@ -14,7 +14,7 @@
* 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 org.matrix.android.sdk.api.session.initsync.InitSyncStep

View file

@ -0,0 +1,60 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.sync.handler
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.getPresenceContent
import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence
import org.matrix.android.sdk.internal.database.query.updateUserPresence
import javax.inject.Inject
internal class PresenceSyncHandler @Inject constructor() {
fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) {
presenceSyncResponse?.events?.filter { event ->
event.type == EventType.PRESENCE
}?.forEach { event ->
val content = event.getPresenceContent() ?: return@forEach
val userId = event.senderId ?: return@forEach
val userPresenceEntity = UserPresenceEntity(
userId = userId,
lastActiveAgo = content.lastActiveAgo,
statusMessage = content.statusMessage,
isCurrentlyActive = content.isCurrentlyActive,
avatarUrl = content.avatarUrl
).also {
it.presence = content.presence
}
storePresenceToDB(realm, userPresenceEntity)
}
}
/**
* Store user presence to DB and update Direct Rooms and Room Member Summaries accordingly
*/
private fun storePresenceToDB(realm: Realm, userPresenceEntity: UserPresenceEntity) =
realm.copyToRealmOrUpdate(userPresenceEntity)?.apply {
RoomSummaryEntity.updateDirectUserPresence(realm, userPresenceEntity.userId, this)
RoomMemberSummaryEntity.updateUserPresence(realm, userPresenceEntity.userId, this)
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,9 +14,11 @@
* 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.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.user.accountdata.DirectChatsHelper
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask

View file

@ -14,7 +14,7 @@
* 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 io.realm.Realm

View file

@ -14,7 +14,7 @@
* 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 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.getOrCreate
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 javax.inject.Inject

View file

@ -14,7 +14,7 @@
* 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 org.matrix.android.sdk.internal.database.model.ReadMarkerEntity

View file

@ -14,7 +14,7 @@
* 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.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.TimelineInput
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.util.computeBestChunkSize
import timber.log.Timber

View file

@ -14,7 +14,7 @@
* 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 org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent

View file

@ -14,7 +14,7 @@
* 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 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.query.getOrCreate
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.RoomTagHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler
import javax.inject.Inject
internal class RoomSyncAccountDataHandler @Inject constructor(private val roomTagHandler: RoomTagHandler,

View file

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

View file

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

View file

@ -0,0 +1,44 @@
/*
* Copyright 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.app.core.epoxy.profiles
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item)
abstract class ProfileMatrixItemWithPresence : BaseProfileMatrixItem<ProfileMatrixItemWithPresence.Holder>() {
@EpoxyAttribute var powerLevelLabel: CharSequence? = null
@EpoxyAttribute var userPresence: UserPresence? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.powerLabel.setTextOrHide(powerLevelLabel)
holder.presenceImageView.render(userPresence = userPresence)
holder.editableView.isVisible = false
}
class Holder : ProfileMatrixItem.Holder() {
val powerLabel by bind<TextView>(R.id.matrixItemPowerLevelLabel)
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.ui.views
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import im.vector.app.R
import org.matrix.android.sdk.internal.session.presence.model.PresenceEnum
import org.matrix.android.sdk.internal.session.presence.model.UserPresence
/**
* Custom ImageView to dynamically render Presence state in multiple screens
*/
class PresenceStateImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
fun render(showPresence: Boolean = true, userPresence: UserPresence?) {
isVisible = showPresence && userPresence != null
when (userPresence?.presence) {
PresenceEnum.ONLINE -> {
setImageResource(R.drawable.ic_presence_online)
contentDescription = context.getString(R.string.a11y_presence_online)
}
PresenceEnum.UNAVAILABLE -> {
setImageResource(R.drawable.ic_presence_offline)
contentDescription = context.getString(R.string.a11y_presence_unavailable)
}
PresenceEnum.OFFLINE -> {
setImageResource(R.drawable.ic_presence_offline)
contentDescription = context.getString(R.string.a11y_presence_offline)
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -45,19 +45,35 @@
<im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/roomToolbarDecorationImageView"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_width="17dp"
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_constraintCircleAngle="135"
app:layout_constraintCircleRadius="20dp"
tools:ignore="MissingConstraints" />
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_presence_offline"
tools:visibility="visible" />
<TextView
android:id="@+id/roomToolbarTitleView"
style="@style/Widget.Vector.TextView.HeadlineMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
@ -65,8 +81,10 @@
app:layout_constraintBottom_toTopOf="@+id/roomToolbarSubtitleView"
app:layout_constraintEnd_toEndOf="parent"
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_goneMarginStart="7dp"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@sample/rooms.json/data/name" />
<TextView
@ -74,9 +92,8 @@
style="@style/Widget.Vector.TextView.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginStart="7dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?vctr_content_primary"
@ -85,7 +102,8 @@
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/roomToolbarAvatarImageView"
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>

View file

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

View file

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

View file

@ -30,14 +30,33 @@
app:layout_constraintVertical_chainStyle="spread_inside"
tools:src="@sample/user_round_avatars" />
<im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/memberProfileDecorationImageView"
android:layout_width="48dp"
android:layout_height="48dp"
<im.vector.app.core.ui.views.PresenceStateImageView
android:id="@+id/memberProfilePresenceImageView"
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_constraintCircleAngle="135"
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
android:id="@+id/memberProfileNameView"
@ -48,7 +67,7 @@
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/memberProfileIdView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@+id/memberProfileDecorationImageView"
app:layout_constraintTop_toBottomOf="@+id/memberProfileAvatarView"
tools:text="@sample/users.json/data/displayName" />

View file

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

View file

@ -3404,6 +3404,9 @@
<string name="a11y_view_read_receipts">View read receipts</string>
<string name="a11y_public_room">Public room</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_explore_room_state">Explore Room State</string>