Merge pull request #7232 from element-hq/feature/fga/display_user_status

WIP : display user status
This commit is contained in:
ganfra
2026-07-23 15:10:19 +02:00
committed by GitHub
153 changed files with 625 additions and 305 deletions
@@ -25,7 +25,7 @@ fun MatrixBadgeRowMolecule(
) {
FlowRow(
modifier = modifier
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
.padding(start = 16.dp, end = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser
data class RoomMember(
@@ -23,6 +24,7 @@ data class RoomMember(
val role: Role,
val membershipChangeReason: String?,
val isServiceMember: Boolean,
val displayedStatus: DisplayedStatus?,
) {
/**
* Role of the RoomMember, based on its [powerLevel].
@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
@Immutable
sealed interface ProfileDetails {
@@ -20,7 +21,8 @@ sealed interface ProfileDetails {
data class Ready(
val displayName: String?,
val displayNameAmbiguous: Boolean,
val avatarUrl: String?
val avatarUrl: String?,
val displayedStatus: DisplayedStatus?,
) : ProfileDetails
data class Error(
@@ -38,6 +38,7 @@ class ProfileTimelineDetailsTest {
displayName = null,
displayNameAmbiguous = false,
avatarUrl = null,
displayedStatus = null,
).getDisambiguatedDisplayName(aUserId)
).isEqualTo(A_USER_ID)
}
@@ -49,6 +50,7 @@ class ProfileTimelineDetailsTest {
displayName = "Alice",
displayNameAmbiguous = false,
avatarUrl = null,
displayedStatus = null,
).getDisambiguatedDisplayName(aUserId)
).isEqualTo("Alice")
}
@@ -60,6 +62,7 @@ class ProfileTimelineDetailsTest {
displayName = "Alice",
displayNameAmbiguous = true,
avatarUrl = null,
displayedStatus = null,
).getDisambiguatedDisplayName(aUserId)
).isEqualTo("Alice ($A_USER_ID)")
}
@@ -30,6 +30,8 @@ object RoomMemberMapper {
role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel),
membershipChangeReason = roomMember.membershipChangeReason,
isServiceMember = roomMember.isServiceMember,
// TODO map this value when available
displayedStatus = null,
)
}
@@ -74,7 +74,9 @@ fun RustProfileDetails.map(): ProfileDetails {
is RustProfileDetails.Ready -> ProfileDetails.Ready(
displayName = displayName,
displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = avatarUrl
avatarUrl = avatarUrl,
// TODO map this value when available
displayedStatus = null,
)
}
}
@@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.test.room
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.collections.immutable.persistentListOf
fun aRoomMember(
@@ -24,6 +25,7 @@ fun aRoomMember(
role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null,
isServiceMember: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = RoomMember(
userId = userId,
displayName = displayName,
@@ -35,6 +37,7 @@ fun aRoomMember(
role = role,
membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember,
displayedStatus = displayedStatus,
)
fun aRoomMemberList() = persistentListOf(
@@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.SendHandlePro
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemDebugInfoProvider
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
@@ -84,11 +85,13 @@ fun anEventTimelineItem(
fun aProfileDetails(
displayName: String? = A_USER_NAME,
displayNameAmbiguous: Boolean = false,
avatarUrl: String? = null
avatarUrl: String? = null,
displayedStatus: DisplayedStatus? = null,
): ProfileDetails = ProfileDetails.Ready(
displayName = displayName,
displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = avatarUrl,
displayedStatus = displayedStatus,
)
fun aProfileChangeMessageContent(
@@ -19,6 +19,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -50,7 +51,7 @@ fun CheckableUserRow(
UserRow(
modifier = rowModifier,
avatarData = data.avatarData,
name = data.name,
name = AnnotatedString(data.name),
subtext = data.subtext,
enabled = enabled,
)
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.model.toEmojiText
/**
* Component to display side by side a display name and an optional status.
*/
@Composable
fun DisplayNameWithStatus(
name: String,
status: DisplayedStatus?,
style: TextStyle,
modifier: Modifier = Modifier,
nameColor: Color = Color.Unspecified,
nameFontStyle: FontStyle? = null,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = name,
modifier = Modifier
.weight(1f, fill = false)
.clipToBounds(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = style,
color = nameColor,
fontStyle = nameFontStyle,
)
if (status != null) {
Spacer(Modifier.width(4.dp))
Text(
text = status.toEmojiText(),
style = style,
color = ElementTheme.colors.textSecondary,
)
}
}
}
@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -58,13 +57,11 @@ fun MatrixUserHeader(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
// Name
Text(
modifier = Modifier.clipToBounds(),
text = matrixUser.getBestName(),
maxLines = 1,
DisplayNameWithStatus(
name = matrixUser.getBestName(),
status = matrixUser.displayedStatus,
nameColor = ElementTheme.colors.textPrimary,
style = ElementTheme.typography.fontHeadingMdRegular,
overflow = TextOverflow.Ellipsis,
color = ElementTheme.colors.textPrimary,
)
// Id
if (matrixUser.displayName.isNullOrEmpty().not()) {
@@ -21,6 +21,7 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_SUSIE
import io.element.android.libraries.designsystem.preview.USER_NAME_VICTOR
import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser
open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
@@ -28,6 +29,7 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
get() = sequenceOf(
aMatrixUser(),
aMatrixUser(displayName = null),
aMatrixUser(displayedStatus = DisplayedStatus.InCall(0L)),
)
}
@@ -43,10 +45,12 @@ fun aMatrixUser(
id: String? = null,
displayName: String? = USER_NAME_ALICE,
avatarUrl: String? = null,
displayedStatus: DisplayedStatus? = null,
) = MatrixUser(
userId = UserId(id ?: "@${displayName?.lowercase()?.replace(" ", "_") ?: "id"}:server.org"),
displayName = displayName,
avatarUrl = avatarUrl,
displayedStatus = displayedStatus,
)
fun aMatrixUserList() = listOf(
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -29,7 +30,14 @@ fun MatrixUserRow(
trailingContent: @Composable (() -> Unit)? = null,
) = UserRow(
avatarData = matrixUser.getAvatarData(avatarSize),
name = matrixUser.getBestName(),
nameContent = {
DisplayNameWithStatus(
name = matrixUser.getBestName(),
status = matrixUser.displayedStatus,
nameColor = ElementTheme.colors.textPrimary,
style = ElementTheme.typography.fontBodyLgRegular,
)
},
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
modifier = modifier,
verticalSpaceWidth = verticalSpaceWidth,
@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -30,7 +31,37 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable
internal fun UserRow(
avatarData: AvatarData,
name: String,
name: AnnotatedString,
subtext: String?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
verticalSpaceWidth: Dp = 12.dp,
trailingContent: @Composable (() -> Unit)? = null,
) {
UserRow(
avatarData = avatarData,
nameContent = {
Text(
modifier = Modifier.clipToBounds(),
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (enabled) ElementTheme.colors.textPrimary else ElementTheme.colors.textDisabled,
style = ElementTheme.typography.fontBodyLgRegular,
)
},
subtext = subtext,
modifier = modifier,
enabled = enabled,
verticalSpaceWidth = verticalSpaceWidth,
trailingContent = trailingContent,
)
}
@Composable
internal fun UserRow(
avatarData: AvatarData,
nameContent: @Composable () -> Unit,
subtext: String?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
@@ -52,14 +83,7 @@ internal fun UserRow(
modifier = Modifier.weight(1f),
) {
// Name
Text(
modifier = Modifier.clipToBounds(),
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (enabled) ElementTheme.colors.textPrimary else ElementTheme.colors.textDisabled,
style = ElementTheme.typography.fontBodyLgRegular,
)
nameContent()
// Id
subtext?.let {
Text(
@@ -13,6 +13,7 @@ import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
@@ -31,4 +32,8 @@ class RoomMemberProfilesCache {
fun getDisplayName(userId: UserId): String? {
return cache.value[userId]?.disambiguatedDisplayName
}
fun getDisplayedStatus(userId: UserId): DisplayedStatus? {
return cache.value[userId]?.displayedStatus
}
}
@@ -167,4 +167,5 @@ fun aProfileDetailsReady(
displayName = displayName,
displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = avatarUrl,
displayedStatus = null,
)
@@ -24,6 +24,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.ui.model.toText
// https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0
@Composable
@@ -54,6 +55,19 @@ fun SenderName(
SecondaryText(text = senderId.value, mode = senderNameMode)
}
}
val userStatus = senderProfile.displayedStatus
if (senderNameMode is SenderNameMode.Timeline && userStatus != null) {
Text(
modifier = Modifier
.clipToBounds()
.alignByBaseline(),
text = userStatus.toText(),
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
@@ -74,13 +88,16 @@ private fun RowScope.MainText(
SenderNameMode.ActionList,
SenderNameMode.Reply -> Modifier
}
val color = when (mode) {
is SenderNameMode.Timeline -> mode.mainColor
SenderNameMode.ActionList,
SenderNameMode.Reply -> ElementTheme.colors.textPrimary
}
Text(
modifier = modifier.clipToBounds(),
modifier = modifier
.clipToBounds()
.weight(1f, fill = false),
text = text,
style = style,
color = color,
@@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.api.user.DisplayedStatus
data class SenderNameData(
val userId: UserId,
@@ -47,12 +48,14 @@ open class SenderNameDataProvider : PreviewParameterProvider<SenderNameData> {
private fun aSenderNameData(
senderNameMode: SenderNameMode,
displayNameAmbiguous: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = SenderNameData(
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
profileDetails = ProfileDetails.Ready(
displayName = "Alice ${senderNameMode.javaClass.simpleName}",
displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = null
avatarUrl = null,
displayedStatus = displayedStatus,
),
senderNameMode = senderNameMode,
)
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.model
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.ui.strings.CommonStrings
fun DisplayedStatus.toEmojiText(): String = when (this) {
is DisplayedStatus.UserSet -> status.emoji
is DisplayedStatus.InCall -> "🎧"
}
@Composable
fun DisplayedStatus.toText(): String = when (this) {
is DisplayedStatus.UserSet -> "${status.emoji} ${status.text}"
is DisplayedStatus.InCall -> "🎧 ${stringResource(CommonStrings.common_on_a_call)}"
}
@@ -13,6 +13,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.user.DisplayedStatus
fun RoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
id = id.value,
@@ -45,3 +46,14 @@ fun RoomInfo.roleOf(userId: UserId): RoomMember.Role {
val powerLevel = powerLevelOf(userId = userId)
return RoomMember.Role.forPowerLevel(powerLevel)
}
/**
* Return the [DisplayedStatus] of the other member in a DM.
*/
fun RoomInfo.dmUserStatus(): DisplayedStatus? {
return if (isDm) {
heroes.firstOrNull()?.displayedStatus
} else {
null
}
}
@@ -21,6 +21,7 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.collections.immutable.persistentListOf
fun aRoomMember(
@@ -34,6 +35,7 @@ fun aRoomMember(
role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null,
isServiceMember: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = RoomMember(
userId = userId,
displayName = displayName,
@@ -45,6 +47,7 @@ fun aRoomMember(
role = role,
membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember,
displayedStatus = displayedStatus,
)
fun aRoomMemberList() = persistentListOf(
@@ -39,11 +39,15 @@ class MentionSpan(
private var typeface: Typeface = Typeface.DEFAULT
private var measuredTextWidth = 0
private var measuredTrailingWidth = 0
// The formatted display text, will be set by the formatter
var displayText: CharSequence = ""
private set
var trailingText: String? = null
private set
/**
* Updates the visual properties of this span.
*/
@@ -80,6 +84,10 @@ class MentionSpan(
displayText = formatter.formatDisplayText(type)
}
fun updateTrailingText(trailingText: String?) {
this.trailingText = trailingText
}
override fun getSize(
paint: Paint,
text: CharSequence?,
@@ -91,7 +99,8 @@ class MentionSpan(
textPaint.typeface = typeface
// Measure the full text width without truncation
measuredTextWidth = textPaint.measureText(displayText, 0, displayText.length).roundToInt()
return measuredTextWidth + startPadding + endPadding
measuredTrailingWidth = trailingText?.let { textPaint.measureText(" $it").roundToInt() } ?: 0
return measuredTextWidth + measuredTrailingWidth + startPadding + endPadding
}
override fun draw(
@@ -106,7 +115,8 @@ class MentionSpan(
paint: Paint
) {
val availableWidth = (canvas.width - x).coerceAtLeast(0f)
val measuredWidth = measuredTextWidth + startPadding + endPadding
val measuredWidth = measuredTextWidth + measuredTrailingWidth + startPadding + endPadding
val pillWidth = minOf(availableWidth, measuredWidth.toFloat())
backgroundPaint.color = backgroundColor
@@ -118,18 +128,18 @@ class MentionSpan(
textPaint.color = textColor
textPaint.typeface = typeface
val availableWidthForText = availableWidth - startPadding - endPadding
val availableWidthForText = availableWidth - startPadding - endPadding - measuredTrailingWidth
val textToDraw = if (measuredTextWidth > availableWidthForText) {
TextUtils.ellipsize(
displayText,
textPaint,
availableWidthForText,
TextUtils.TruncateAt.END
)
TextUtils.ellipsize(displayText, textPaint, availableWidthForText, TextUtils.TruncateAt.END)
} else {
displayText
}
canvas.drawText(textToDraw, 0, textToDraw.length, x + startPadding, y.toFloat(), textPaint)
trailingText?.let {
val drawnTextWidth = minOf(availableWidthForText, measuredTextWidth.toFloat())
canvas.drawText(" $it", x + startPadding + drawnTextWidth, y.toFloat(), textPaint)
}
}
}
@@ -18,6 +18,7 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
import io.element.android.libraries.matrix.ui.messages.RoomNamesCache
import io.element.android.libraries.matrix.ui.model.toEmojiText
interface MentionSpanUpdater {
fun updateMentionSpans(text: CharSequence): CharSequence
@@ -48,6 +49,13 @@ class DefaultMentionSpanUpdater(
for (mentionSpan in text.getMentionSpans()) {
mentionSpan.updateTheme(theme)
mentionSpan.updateDisplayText(formatter)
mentionSpan.updateTrailingText(
if (mentionSpan.type is MentionType.User) {
roomMemberProfilesCache.getDisplayedStatus(mentionSpan.type.userId)?.toEmojiText()
} else {
null
}
)
}
return text
}
@@ -23,6 +23,7 @@
<string name="a11y_join_call">"Join call"</string>
<string name="a11y_jump_to_bottom">"Jump to bottom"</string>
<string name="a11y_jump_to_unread">"Jump to unread"</string>
<string name="a11y_jump_to_unread_messages">"Jump to first unread message"</string>
<string name="a11y_move_the_map_to_my_location">"Move the map to my location"</string>
<string name="a11y_notifications_mentions_only">"Mentions only"</string>
<string name="a11y_notifications_muted">"Muted"</string>
@@ -143,6 +144,7 @@
<string name="action_manage_account_and_devices">"Manage account &amp; devices"</string>
<string name="action_manage_devices">"Manage devices"</string>
<string name="action_manage_rooms">"Manage rooms"</string>
<string name="action_mark_as_read">"Mark as read"</string>
<string name="action_message">"Message"</string>
<string name="action_minimize">"Minimise"</string>
<string name="action_next">"Next"</string>
@@ -515,6 +517,8 @@ Are you sure you want to continue?"</string>
<item quantity="other">"%1$d people"</item>
</plurals>
<string name="screen_live_location_sheet_title">"On the map"</string>
<string name="screen_local_network_opt_in_subtitle">"Your homeserver is on your local network. To connect, Element needs permission to reach devices on this network."</string>
<string name="screen_local_network_opt_in_title">"Allow access to local network"</string>
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
<string name="screen_pinned_timeline_empty_state_description">"Press on a message and choose “%1$s” to include here."</string>
<string name="screen_pinned_timeline_empty_state_headline">"Pin important messages so that they can be easily discovered"</string>
@@ -6,12 +6,8 @@
-->
<resources>
<string name="action_mark_as_read">"Mark as read"</string>
<string name="a11y_jump_to_unread_messages">"Jump to first unread message"</string>
<string name="a11y_state_expanded">"Expanded"</string>
<string name="a11y_state_collapsed">"Collapsed"</string>
<string name="screen_local_network_opt_in_title">Allow access to local network</string>
<string name="screen_local_network_opt_in_subtitle">Your homeserver is on your local network. To connect, Element needs permission to reach other devices on this network.</string>
<string name="a11y_active">Active</string>
<string name="a11y_inactive">Inactive</string>
</resources>