Merge branch 'develop' into feature/valere/timeline_active_call_rendering

# Conflicts:
#	tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Night_0_en.png
This commit is contained in:
ganfra
2026-07-23 16:24:11 +02:00
347 changed files with 2066 additions and 1252 deletions
+34
View File
@@ -1,5 +1,39 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" /> <option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
@@ -28,8 +28,6 @@ import io.element.android.libraries.di.BaseDirectory
import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.recentemojis.api.EmojibaseProvider
import io.element.android.libraries.recentemojis.impl.DefaultEmojibaseProvider
import io.element.android.x.BuildConfig import io.element.android.x.BuildConfig
import io.element.android.x.R import io.element.android.x.R
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
@@ -115,10 +113,4 @@ object AppModule {
fun provideSnackbarDispatcher(): SnackbarDispatcher { fun provideSnackbarDispatcher(): SnackbarDispatcher {
return SnackbarDispatcher() return SnackbarDispatcher()
} }
@Provides
@SingleIn(AppScope::class)
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
return DefaultEmojibaseProvider(context)
}
} }
@@ -63,6 +63,8 @@ import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.designsystem.theme.unreadIndicator import io.element.android.libraries.designsystem.theme.unreadIndicator
import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
import io.element.android.libraries.matrix.ui.components.InviteSenderView import io.element.android.libraries.matrix.ui.components.InviteSenderView
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@@ -126,7 +128,8 @@ internal fun RoomSummaryRow(
NameAndTimestampRow( NameAndTimestampRow(
name = room.name, name = room.name,
timestamp = room.timestamp, timestamp = room.timestamp,
isHighlighted = room.isHighlighted isHighlighted = room.isHighlighted,
dmUserStatus = room.dmUserStatus,
) )
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount) MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
} }
@@ -142,7 +145,8 @@ internal fun RoomSummaryRow(
NameAndTimestampRow( NameAndTimestampRow(
name = room.name, name = room.name,
timestamp = null, timestamp = null,
isHighlighted = room.isHighlighted isHighlighted = room.isHighlighted,
dmUserStatus = null,
) )
if (room.canonicalAlias != null) { if (room.canonicalAlias != null) {
Text( Text(
@@ -218,22 +222,21 @@ private fun NameAndTimestampRow(
name: String?, name: String?,
timestamp: String?, timestamp: String?,
isHighlighted: Boolean, isHighlighted: Boolean,
dmUserStatus: DisplayedStatus?,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalArrangement = spacedBy(16.dp) horizontalArrangement = spacedBy(16.dp)
) { ) {
Text( val displayName = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name)
modifier = Modifier DisplayNameWithStatus(
.weight(1f) name = displayName,
.clipToBounds(), status = dmUserStatus,
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyLgMedium, style = ElementTheme.typography.fontBodyLgMedium,
text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name), nameColor = ElementTheme.colors.roomListRoomName,
fontStyle = FontStyle.Italic.takeIf { name == null }, nameFontStyle = FontStyle.Italic.takeIf { name == null },
color = ElementTheme.colors.roomListRoomName,
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
// Timestamp // Timestamp
Text( Text(
@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.dmUserStatus
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -76,6 +77,7 @@ class RoomListRoomSummaryFactory(
}.toImmutableList(), }.toImmutableList(),
isTombstoned = roomInfo.successorRoom != null, isTombstoned = roomInfo.successorRoom != null,
isSpace = roomInfo.isSpace, isSpace = roomInfo.isSpace,
dmUserStatus = roomInfo.dmUserStatus(),
) )
} }
@@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -42,6 +43,7 @@ data class RoomListRoomSummary(
val isTombstoned: Boolean, val isTombstoned: Boolean,
val heroes: ImmutableList<AvatarData>, val heroes: ImmutableList<AvatarData>,
val isSpace: Boolean, val isSpace: Boolean,
val dmUserStatus: DisplayedStatus?,
) { ) {
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE && val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) || (numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -182,6 +183,7 @@ internal fun aRoomListRoomSummary(
heroes: List<AvatarData> = emptyList(), heroes: List<AvatarData> = emptyList(),
isTombstoned: Boolean = false, isTombstoned: Boolean = false,
isSpace: Boolean = false, isSpace: Boolean = false,
dmUserStatus: DisplayedStatus? = null,
) = RoomListRoomSummary( ) = RoomListRoomSummary(
id = id, id = id,
roomId = RoomId(id), roomId = RoomId(id),
@@ -204,5 +206,6 @@ internal fun aRoomListRoomSummary(
canonicalAlias = canonicalAlias, canonicalAlias = canonicalAlias,
heroes = heroes.toImmutableList(), heroes = heroes.toImmutableList(),
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
isSpace = isSpace isSpace = isSpace,
dmUserStatus = dmUserStatus,
) )
@@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -87,6 +88,7 @@ internal fun createRoomListRoomSummary(
timestamp: String? = null, timestamp: String? = null,
isTombstoned: Boolean = false, isTombstoned: Boolean = false,
isSpace: Boolean = false, isSpace: Boolean = false,
dmUserStatus: DisplayedStatus? = null,
) = RoomListRoomSummary( ) = RoomListRoomSummary(
id = A_ROOM_ID.value, id = A_ROOM_ID.value,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@@ -109,5 +111,6 @@ internal fun createRoomListRoomSummary(
isDm = false, isDm = false,
heroes = heroes.toImmutableList(), heroes = heroes.toImmutableList(),
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
isSpace = isSpace isSpace = isSpace,
dmUserStatus = dmUserStatus,
) )
@@ -9,21 +9,25 @@
package io.element.android.features.linknewdevice.impl.screens.desktop package io.element.android.features.linknewdevice.impl.screens.desktop
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.PreviewParameter 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.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.linknewdevice.impl.R import io.element.android.features.linknewdevice.impl.R
import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism
@@ -34,6 +38,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.LocalBuildMeta import io.element.android.libraries.designsystem.theme.LocalBuildMeta
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
import io.element.android.libraries.permissions.api.PermissionsView import io.element.android.libraries.permissions.api.PermissionsView
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -71,11 +76,10 @@ fun DesktopNoticeView(
} }
) { ) {
Column( Column(
Modifier.fillMaxWidth() Modifier.fillMaxWidth(),
) { ) {
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))
NumberedListOrganism( NumberedListOrganism(
modifier = Modifier.fillMaxSize(),
items = persistentListOf( items = persistentListOf(
AnnotatedString(stringResource(R.string.screen_link_new_device_desktop_step1, appName)), AnnotatedString(stringResource(R.string.screen_link_new_device_desktop_step1, appName)),
annotatedTextWithBold( annotatedTextWithBold(
@@ -88,6 +92,17 @@ fun DesktopNoticeView(
AnnotatedString(stringResource(R.string.screen_link_new_device_desktop_step3)), AnnotatedString(stringResource(R.string.screen_link_new_device_desktop_step3)),
) )
) )
Spacer(modifier = Modifier.height(32.dp))
Text(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(ElementTheme.colors.bgCriticalSubtle)
.padding(16.dp)
.fillMaxWidth(),
text = stringResource(R.string.screen_link_new_device_desktop_warning),
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textCriticalPrimary,
)
} }
} }
@@ -5,6 +5,7 @@
<string name="screen_link_new_device_desktop_step3">"Scan the QR code with this device"</string> <string name="screen_link_new_device_desktop_step3">"Scan the QR code with this device"</string>
<string name="screen_link_new_device_desktop_submit">"Ready to scan"</string> <string name="screen_link_new_device_desktop_submit">"Ready to scan"</string>
<string name="screen_link_new_device_desktop_title">"Open %1$s on a desktop computer to get the QR code"</string> <string name="screen_link_new_device_desktop_title">"Open %1$s on a desktop computer to get the QR code"</string>
<string name="screen_link_new_device_desktop_warning">"Only scan a QR code after following the steps above on your own device. Never scan a code sent by another person."</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"The numbers don’t match"</string> <string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"The numbers don’t match"</string>
<string name="screen_link_new_device_enter_number_notice">"Enter 2-digit code"</string> <string name="screen_link_new_device_enter_number_notice">"Enter 2-digit code"</string>
<string name="screen_link_new_device_enter_number_subtitle">"This will verify that the connection to your other device is secure."</string> <string name="screen_link_new_device_enter_number_subtitle">"This will verify that the connection to your other device is secure."</string>
+3 -2
View File
@@ -51,7 +51,7 @@ dependencies {
implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.permissions.api) implementation(projects.libraries.permissions.api)
implementation(projects.libraries.preferences.api) implementation(projects.libraries.preferences.api)
implementation(projects.libraries.recentemojis.api) implementation(projects.libraries.emoji.api)
implementation(projects.libraries.roomselect.api) implementation(projects.libraries.roomselect.api)
implementation(projects.libraries.slashcommands.api) implementation(projects.libraries.slashcommands.api)
implementation(projects.libraries.audio.api) implementation(projects.libraries.audio.api)
@@ -106,6 +106,7 @@ dependencies {
testImplementation(projects.libraries.testtags) testImplementation(projects.libraries.testtags)
testImplementation(projects.features.poll.test) testImplementation(projects.features.poll.test)
testImplementation(projects.libraries.eventformatter.test) testImplementation(projects.libraries.eventformatter.test)
testImplementation(projects.libraries.recentemojis.test) testImplementation(projects.libraries.emoji.impl)
testImplementation(projects.libraries.emoji.test)
testImplementation(projects.libraries.slashcommands.test) testImplementation(projects.libraries.slashcommands.test)
} }
@@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.TimelineEvent
import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
@@ -54,6 +55,7 @@ import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@@ -100,6 +102,7 @@ class MessagesNode(
private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer, private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer,
private val roomMemberModerationRenderer: RoomMemberModerationRenderer, private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
private val eventContentValidationCache: EventContentValidationCache, private val eventContentValidationCache: EventContentValidationCache,
private val emojiPickerRenderer: EmojiPickerRenderer,
) : Node(buildContext, plugins = plugins), MessagesNavigator { ) : Node(buildContext, plugins = plugins), MessagesNavigator {
data class Inputs( data class Inputs(
val focusedEventId: EventId?, val focusedEventId: EventId?,
@@ -329,6 +332,15 @@ class MessagesNode(
onViewRequestsClick = callback::navigateToKnockRequestsList, onViewRequestsClick = callback::navigateToKnockRequestsList,
) )
}, },
customReactionBottomSheet = {
CustomReactionBottomSheet(
state = state.customReactionState,
onSelectEmoji = { uniqueId, emoji ->
state.eventSink(MessagesEvent.ToggleReaction(emoji.unicode, uniqueId))
},
emojiPickerRenderer = emojiPickerRenderer,
)
},
onThreadsListClick = callback::navigateToThreadsList, onThreadsListClick = callback::navigateToThreadsList,
) )
roomMemberModerationRenderer.Render( roomMemberModerationRenderer.Render(
@@ -69,6 +69,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.emoji.api.recentemojis.AddRecentEmoji
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.toThreadId import io.element.android.libraries.matrix.api.core.toThreadId
@@ -83,9 +84,9 @@ import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsSta
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.map import io.element.android.libraries.matrix.ui.messages.reply.map
import io.element.android.libraries.matrix.ui.model.dmUserStatus
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.recentemojis.api.AddRecentEmoji
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@@ -319,6 +320,7 @@ class MessagesPresenter(
appName = buildMeta.applicationName, appName = buildMeta.applicationName,
pinnedMessagesBannerState = pinnedMessagesBannerState, pinnedMessagesBannerState = pinnedMessagesBannerState,
dmUserVerificationState = dmUserVerificationState, dmUserVerificationState = dmUserVerificationState,
dmUserStatus = roomInfo.dmUserStatus(),
roomMemberModerationState = roomMemberModerationState, roomMemberModerationState = roomMemberModerationState,
topBarSharedHistoryIcon = topBarSharedHistoryIcon, topBarSharedHistoryIcon = topBarSharedHistoryIcon,
successorRoom = roomInfo.successorRoom, successorRoom = roomInfo.successorRoom,
@@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class MessagesState( data class MessagesState(
@@ -53,6 +54,7 @@ data class MessagesState(
val appName: String, val appName: String,
val pinnedMessagesBannerState: PinnedMessagesBannerState, val pinnedMessagesBannerState: PinnedMessagesBannerState,
val dmUserVerificationState: IdentityState?, val dmUserVerificationState: IdentityState?,
val dmUserStatus: DisplayedStatus?,
val roomMemberModerationState: RoomMemberModerationState, val roomMemberModerationState: RoomMemberModerationState,
/** Type of "shared history" icon to show in the top bar. */ /** Type of "shared history" icon to show in the top bar. */
val topBarSharedHistoryIcon: SharedHistoryIcon, val topBarSharedHistoryIcon: SharedHistoryIcon,
@@ -50,10 +50,10 @@ import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
@@ -129,6 +129,7 @@ fun aMessagesState(
hasUnreadThreads = false, hasUnreadThreads = false,
), ),
isCurrentlySharingLiveLocationInRoom: Boolean = false, isCurrentlySharingLiveLocationInRoom: Boolean = false,
dmUserStatus: DisplayedStatus? = null,
eventSink: (MessagesEvent) -> Unit = {}, eventSink: (MessagesEvent) -> Unit = {},
) = MessagesState( ) = MessagesState(
roomId = RoomId("!id:domain"), roomId = RoomId("!id:domain"),
@@ -159,6 +160,7 @@ fun aMessagesState(
successorRoom = successorRoom, successorRoom = successorRoom,
threads = threads, threads = threads,
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom, showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
dmUserStatus = dmUserStatus,
eventSink = eventSink, eventSink = eventSink,
) )
@@ -193,11 +195,9 @@ fun aReactionSummaryState(
fun aCustomReactionState( fun aCustomReactionState(
target: CustomReactionState.Target = CustomReactionState.Target.None, target: CustomReactionState.Target = CustomReactionState.Target.None,
recentEmojis: ImmutableList<String> = persistentListOf(),
eventSink: (CustomReactionEvent) -> Unit = {}, eventSink: (CustomReactionEvent) -> Unit = {},
) = CustomReactionState( ) = CustomReactionState(
target = target, target = target,
recentEmojis = recentEmojis,
selectedEmoji = persistentSetOf(), selectedEmoji = persistentSetOf(),
eventSink = eventSink, eventSink = eventSink,
) )
@@ -81,7 +81,6 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemDaySepara
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.aTimelineState import io.element.android.features.messages.impl.timeline.aTimelineState
import io.element.android.features.messages.impl.timeline.components.CallMenuItem import io.element.android.features.messages.impl.timeline.components.CallMenuItem
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
@@ -146,9 +145,10 @@ fun MessagesView(
onJoinCallClick: (isAudioCall: Boolean) -> Unit, onJoinCallClick: (isAudioCall: Boolean) -> Unit,
onViewAllPinnedMessagesClick: () -> Unit, onViewAllPinnedMessagesClick: () -> Unit,
onThreadsListClick: () -> Unit, onThreadsListClick: () -> Unit,
knockRequestsBannerView: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false, forceJumpToBottomVisibility: Boolean = false,
knockRequestsBannerView: @Composable () -> Unit, customReactionBottomSheet: @Composable () -> Unit,
) { ) {
val eventContentValidationState = LocalEventContentValidationState.current val eventContentValidationState = LocalEventContentValidationState.current
@@ -244,6 +244,7 @@ fun MessagesView(
heroes = state.heroes, heroes = state.heroes,
dmUserIdentityState = state.dmUserVerificationState, dmUserIdentityState = state.dmUserVerificationState,
sharedHistoryIcon = state.topBarSharedHistoryIcon, sharedHistoryIcon = state.topBarSharedHistoryIcon,
dmUserStatus = state.dmUserStatus,
onBackClick = { hidingKeyboard { onBackClick() } }, onBackClick = { hidingKeyboard { onBackClick() } },
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } }, onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
menuActions = { menuActions = {
@@ -410,12 +411,7 @@ fun MessagesView(
}, },
) )
CustomReactionBottomSheet( customReactionBottomSheet()
state = state.customReactionState,
onSelectEmoji = { uniqueId, emoji ->
state.eventSink(MessagesEvent.ToggleReaction(emoji.unicode, uniqueId))
}
)
ReactionSummaryView(state = state.reactionSummaryState) ReactionSummaryView(state = state.reactionSummaryState)
ReadReceiptBottomSheet( ReadReceiptBottomSheet(
@@ -672,6 +668,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
onViewAllPinnedMessagesClick = { }, onViewAllPinnedMessagesClick = { },
forceJumpToBottomVisibility = true, forceJumpToBottomVisibility = true,
knockRequestsBannerView = {}, knockRequestsBannerView = {},
customReactionBottomSheet = {},
onThreadsListClick = {}, onThreadsListClick = {},
) )
} }
@@ -729,5 +726,6 @@ internal fun MessagesViewA11yPreview() = ElementPreview {
onThreadsListClick = {}, onThreadsListClick = {},
forceJumpToBottomVisibility = true, forceJumpToBottomVisibility = true,
knockRequestsBannerView = {}, knockRequestsBannerView = {},
customReactionBottomSheet = {},
) )
} }
@@ -44,13 +44,13 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.recentemojis.api.GetRecentEmojis
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -43,6 +43,7 @@ internal fun MessagesViewWithIdentityChangePreview(
onJoinCallClick = {}, onJoinCallClick = {},
onViewAllPinnedMessagesClick = {}, onViewAllPinnedMessagesClick = {},
knockRequestsBannerView = {}, knockRequestsBannerView = {},
customReactionBottomSheet = {},
onThreadsListClick = {}, onThreadsListClick = {},
) )
} }
@@ -41,6 +41,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId 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.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
@@ -112,12 +114,16 @@ private fun SuggestionItemView(
is ResolvedSuggestion.Member -> AvatarType.User is ResolvedSuggestion.Member -> AvatarType.User
is ResolvedSuggestion.Command -> null is ResolvedSuggestion.Command -> null
} }
val title = when (suggestion) { val title: String? = when (suggestion) {
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title) is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName is ResolvedSuggestion.Member -> suggestion.roomMember.getBestName()
is ResolvedSuggestion.Alias -> suggestion.roomName is ResolvedSuggestion.Alias -> suggestion.roomName
is ResolvedSuggestion.Command -> suggestion.command.command is ResolvedSuggestion.Command -> suggestion.command.command
} }
val status = when (suggestion) {
is ResolvedSuggestion.Member -> suggestion.roomMember.displayedStatus
else -> null
}
val details = when (suggestion) { val details = when (suggestion) {
is ResolvedSuggestion.AtRoom, is ResolvedSuggestion.AtRoom,
is ResolvedSuggestion.Member, is ResolvedSuggestion.Member,
@@ -148,11 +154,11 @@ private fun SuggestionItemView(
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
title?.let { title?.let {
Text( DisplayNameWithStatus(
text = it, name = title,
status = status,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
maxLines = 1, nameColor = ElementTheme.colors.textPrimary,
overflow = TextOverflow.Ellipsis,
) )
} }
details?.let { details?.let {
@@ -191,6 +197,7 @@ internal fun SuggestionsPickerViewPreview() {
role = RoomMember.Role.User, role = RoomMember.Role.User,
membershipChangeReason = null, membershipChangeReason = null,
isServiceMember = false, isServiceMember = false,
displayedStatus = null,
) )
val anAlias = remember { RoomAlias("#room:domain.org") } val anAlias = remember { RoomAlias("#room:domain.org") }
SuggestionsPickerView( SuggestionsPickerView(
@@ -322,6 +322,7 @@ class ThreadedMessagesNode(
onViewAllPinnedMessagesClick = {}, onViewAllPinnedMessagesClick = {},
modifier = modifier, modifier = modifier,
knockRequestsBannerView = {}, knockRequestsBannerView = {},
customReactionBottomSheet = {},
onThreadsListClick = {}, onThreadsListClick = {},
) )
@@ -383,7 +383,12 @@ fun aThreadListItem(
fun aThreadListItemEvent( fun aThreadListItemEvent(
threadId: ThreadId = ThreadId("\$a-thread-id"), threadId: ThreadId = ThreadId("\$a-thread-id"),
senderId: UserId = UserId("@a-user-id:server"), senderId: UserId = UserId("@a-user-id:server"),
senderProfile: ProfileDetails = ProfileDetails.Ready(displayName = USER_NAME_ALICE, displayNameAmbiguous = false, avatarUrl = null), senderProfile: ProfileDetails = ProfileDetails.Ready(
displayName = USER_NAME_ALICE,
displayNameAmbiguous = false,
avatarUrl = null,
displayedStatus = null,
),
isOwn: Boolean = false, isOwn: Boolean = false,
content: EventContent = MessageContent( content: EventContent = MessageContent(
body = "Hello world!", body = "Hello world!",
@@ -166,6 +166,7 @@ internal fun MessageShieldViewPreview() {
displayName = USER_NAME_ALICE, displayName = USER_NAME_ALICE,
displayNameAmbiguous = false, displayNameAmbiguous = false,
avatarUrl = null, avatarUrl = null,
displayedStatus = null,
), ),
) )
) )
@@ -315,7 +315,12 @@ private fun inReplyToInvalidContent(): InReplyToDetails.Ready = InReplyToDetails
) )
), ),
textContent = "A text content", textContent = "A text content",
senderProfile = ProfileDetails.Ready(displayName = "Sender", displayNameAmbiguous = false, avatarUrl = null) senderProfile = ProfileDetails.Ready(
displayName = "Sender",
displayNameAmbiguous = false,
avatarUrl = null,
displayedStatus = null
)
) )
private fun inReplyToTextContent(): InReplyToDetails.Ready = InReplyToDetails.Ready( private fun inReplyToTextContent(): InReplyToDetails.Ready = InReplyToDetails.Ready(
@@ -332,5 +337,10 @@ private fun inReplyToTextContent(): InReplyToDetails.Ready = InReplyToDetails.Re
) )
), ),
textContent = "A text content", textContent = "A text content",
senderProfile = ProfileDetails.Ready(displayName = "Sender", displayNameAmbiguous = false, avatarUrl = null) senderProfile = ProfileDetails.Ready(
displayName = "Sender",
displayNameAmbiguous = false,
avatarUrl = null,
displayedStatus = null
)
) )
@@ -962,6 +962,7 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
displayName = USER_NAME_ALICE, displayName = USER_NAME_ALICE,
avatarUrl = null, avatarUrl = null,
displayNameAmbiguous = false, displayNameAmbiguous = false,
displayedStatus = null,
), ),
timestamp = 0L, timestamp = 0L,
) )
@@ -1140,6 +1141,7 @@ internal fun ThreadSummaryViewPreview() {
displayName = USER_NAME_ALICE, displayName = USER_NAME_ALICE,
avatarUrl = null, avatarUrl = null,
displayNameAmbiguous = true, displayNameAmbiguous = true,
displayedStatus = null,
), ),
timestamp = 0L, timestamp = 0L,
) )
@@ -13,15 +13,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetValue import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetState import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.Emoji
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPicker import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.EmojiPickerPresenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.hide import io.element.android.libraries.designsystem.theme.components.hide
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -29,46 +27,43 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransa
fun CustomReactionBottomSheet( fun CustomReactionBottomSheet(
state: CustomReactionState, state: CustomReactionState,
onSelectEmoji: (EventOrTransactionId, Emoji) -> Unit, onSelectEmoji: (EventOrTransactionId, Emoji) -> Unit,
emojiPickerRenderer: EmojiPickerRenderer,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val sheetState = rememberBottomSheetState( val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden)
initialValue = SheetValue.Hidden,
)
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val target = state.target as? CustomReactionState.Target.Success
when (state.target) {
CustomReactionState.Target.None,
is CustomReactionState.Target.Loading -> Unit
is CustomReactionState.Target.Success -> {
fun onEmojiSelectedDismiss(emoji: Emoji) {
sheetState.hide(coroutineScope) {
state.eventSink(CustomReactionEvent.DismissCustomReactionSheet)
onSelectEmoji(state.target.event.eventOrTransactionId, emoji)
}
}
fun onDismiss() { fun onDismiss() {
state.eventSink(CustomReactionEvent.DismissCustomReactionSheet) state.eventSink(CustomReactionEvent.DismissCustomReactionSheet)
} }
fun onEmojiSelectedDismiss(emoji: Emoji) {
if (target?.event == null) return
sheetState.hide(coroutineScope) {
state.eventSink(CustomReactionEvent.DismissCustomReactionSheet)
onSelectEmoji(target.event.eventOrTransactionId, emoji)
}
}
if (target?.emojibaseStore != null && target.event.eventId != null) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = ::onDismiss, onDismissRequest = ::onDismiss,
sheetState = sheetState, sheetState = sheetState,
modifier = modifier, modifier = modifier,
scrollable = false, scrollable = false,
) { ) {
val presenter = remember { emojiPickerRenderer.Render(
EmojiPickerPresenter( state = state.target.emojiPickerState,
emojibaseStore = target.emojibaseStore,
recentEmojis = state.recentEmojis,
coroutineDispatchers = CoroutineDispatchers.Default,
)
}
EmojiPicker(
onSelectEmoji = ::onEmojiSelectedDismiss, onSelectEmoji = ::onEmojiSelectedDismiss,
state = presenter.present(),
selectedEmojis = state.selectedEmoji, selectedEmojis = state.selectedEmoji,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentDescription = { emoji, isSelected ->
a11yReactionAction(emoji = emoji.unicode, userAlreadyReacted = isSelected)
},
) )
} }
} }
}
} }
@@ -9,59 +9,53 @@
package io.element.android.features.messages.impl.timeline.components.customreaction package io.element.android.features.messages.impl.timeline.components.customreaction
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.recentemojis.api.EmojibaseProvider import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter
import io.element.android.libraries.recentemojis.api.GetRecentEmojis import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.launch
@Inject @Inject
class CustomReactionPresenter( class CustomReactionPresenter(
private val emojibaseProvider: EmojibaseProvider, emojiPickerPresenterFactory: EmojiPickerPresenter.Factory,
private val getRecentEmojis: GetRecentEmojis, getRecentEmojis: GetRecentEmojis,
) : Presenter<CustomReactionState> { ) : Presenter<CustomReactionState> {
private val emojiPickerPresenter = emojiPickerPresenterFactory.create(getRecentEmojis)
@Composable @Composable
override fun present(): CustomReactionState { override fun present(): CustomReactionState {
val localCoroutineScope = rememberCoroutineScope() var internalTarget by remember { mutableStateOf<InternalTarget>(InternalTarget.None) }
var recentEmojis by remember { mutableStateOf<ImmutableList<String>>(persistentListOf()) } val emojiPickerState = emojiPickerPresenter.present()
val target: MutableState<CustomReactionState.Target> = remember {
mutableStateOf(CustomReactionState.Target.None)
}
fun handleShowCustomReactionSheet(event: TimelineItem.Event) {
target.value = CustomReactionState.Target.Loading(event)
localCoroutineScope.launch {
recentEmojis = getRecentEmojis().getOrNull() ?: persistentListOf()
target.value = CustomReactionState.Target.Success(
event = event,
emojibaseStore = emojibaseProvider.emojibaseStore
)
}
}
fun handleDismissCustomReactionSheet() {
target.value = CustomReactionState.Target.None
}
fun handleEvent(event: CustomReactionEvent) { fun handleEvent(event: CustomReactionEvent) {
when (event) { when (event) {
is CustomReactionEvent.ShowCustomReactionSheet -> handleShowCustomReactionSheet(event.event) is CustomReactionEvent.ShowCustomReactionSheet -> {
is CustomReactionEvent.DismissCustomReactionSheet -> handleDismissCustomReactionSheet() internalTarget = InternalTarget.Showing(event.event)
}
is CustomReactionEvent.DismissCustomReactionSheet -> {
internalTarget = InternalTarget.None
} }
} }
val event = (target.value as? CustomReactionState.Target.Success)?.event }
val selectedEmoji = event
val computedTarget = when (val target = internalTarget) {
InternalTarget.None -> CustomReactionState.Target.None
is InternalTarget.Showing -> if (emojiPickerState.isReady) {
CustomReactionState.Target.Success(
event = target.event,
emojiPickerState = emojiPickerState,
)
} else {
CustomReactionState.Target.Loading(target.event)
}
}
val eventForSelection = (computedTarget as? CustomReactionState.Target.Success)?.event
val selectedEmoji = eventForSelection
?.reactionsState ?.reactionsState
?.reactions ?.reactions
?.mapNotNull { if (it.isHighlighted) it.key else null } ?.mapNotNull { if (it.isHighlighted) it.key else null }
@@ -69,10 +63,14 @@ class CustomReactionPresenter(
.toImmutableSet() .toImmutableSet()
return CustomReactionState( return CustomReactionState(
target = target.value, target = computedTarget,
selectedEmoji = selectedEmoji, selectedEmoji = selectedEmoji,
recentEmojis = recentEmojis,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
} }
private sealed interface InternalTarget {
data object None : InternalTarget
data class Showing(val event: TimelineItem.Event) : InternalTarget
}
@@ -8,15 +8,13 @@
package io.element.android.features.messages.impl.timeline.components.customreaction package io.element.android.features.messages.impl.timeline.components.customreaction
import io.element.android.emojibasebindings.EmojibaseStore
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import kotlinx.collections.immutable.ImmutableList import io.element.android.libraries.emoji.api.picker.EmojiPickerState
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
data class CustomReactionState( data class CustomReactionState(
val target: Target, val target: Target,
val selectedEmoji: ImmutableSet<String>, val selectedEmoji: ImmutableSet<String>,
val recentEmojis: ImmutableList<String>,
val eventSink: (CustomReactionEvent) -> Unit, val eventSink: (CustomReactionEvent) -> Unit,
) { ) {
sealed interface Target { sealed interface Target {
@@ -24,7 +22,7 @@ data class CustomReactionState(
data class Loading(val event: TimelineItem.Event) : Target data class Loading(val event: TimelineItem.Event) : Target
data class Success( data class Success(
val event: TimelineItem.Event, val event: TimelineItem.Event,
val emojibaseStore: EmojibaseStore, val emojiPickerState: EmojiPickerState,
) : Target ) : Target
} }
} }
@@ -1,114 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector 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.features.messages.impl.timeline.components.customreaction.picker
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalInspectionMode
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.emojibasebindings.Emoji
import io.element.android.emojibasebindings.EmojibaseStore
import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.timeline.components.customreaction.icon
import io.element.android.features.messages.impl.timeline.components.customreaction.title
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.milliseconds
class EmojiPickerPresenter(
private val emojibaseStore: EmojibaseStore,
private val recentEmojis: ImmutableList<String>,
private val coroutineDispatchers: CoroutineDispatchers,
) : Presenter<EmojiPickerState> {
@Composable
override fun present(): EmojiPickerState {
val queryState = rememberTextFieldState()
var isSearchActive by remember { mutableStateOf(false) }
var emojiResults by remember { mutableStateOf<SearchBarResultState<ImmutableList<Emoji>>>(SearchBarResultState.Initial()) }
val recentEmojiIcon = CompoundIcons.History()
val categories = remember {
val providedCategories = emojibaseStore.categories.map { (category, emojis) ->
EmojiCategory(
titleId = category.title,
icon = IconSource.Vector(category.icon),
emojis = emojis
)
}
if (recentEmojis.isNotEmpty()) {
val recentEmojis = recentEmojis.mapNotNull { recentEmoji ->
emojibaseStore.allEmojis.find { it.unicode == recentEmoji }
}.toImmutableList()
val recentCategory =
EmojiCategory(
titleId = R.string.emoji_picker_category_recent,
icon = IconSource.Vector(recentEmojiIcon),
emojis = recentEmojis
)
(listOf(recentCategory) + providedCategories).toImmutableList()
} else {
providedCategories.toImmutableList()
}
}
val searchQuery = queryState.text.toString()
LaunchedEffect(searchQuery) {
emojiResults = if (searchQuery.isEmpty()) {
SearchBarResultState.Initial()
} else {
// Add a small delay to avoid doing too many computations when the user is typing quickly
delay(100.milliseconds)
val lowercaseQuery = searchQuery.lowercase()
val results = withContext(coroutineDispatchers.computation) {
emojibaseStore.allEmojis
.asSequence()
.filter { emoji ->
emoji.tags.orEmpty().any { it.contains(lowercaseQuery) } ||
emoji.shortcodes.any { it.contains(lowercaseQuery) }
}
.take(60)
.toImmutableList()
}
SearchBarResultState.Results(results)
}
}
val isInPreview = LocalInspectionMode.current
fun handleEvent(event: EmojiPickerEvent) {
when (event) {
// For some reason, in preview mode the SearchBar emits this event with an `isActive = true` value automatically
is EmojiPickerEvent.ToggleSearchActive -> if (!isInPreview) {
isSearchActive = event.isActive
}
}
}
return EmojiPickerState(
categories = categories,
allEmojis = emojibaseStore.allEmojis,
searchQuery = queryState,
isSearchActive = isSearchActive,
searchResults = emojiResults,
eventSink = ::handleEvent,
)
}
}
@@ -26,7 +26,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
@@ -46,9 +45,10 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ROOM_NAME import io.element.android.libraries.designsystem.preview.ROOM_NAME
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@@ -65,6 +65,7 @@ internal fun MessagesViewTopBar(
heroes: ImmutableList<AvatarData>, heroes: ImmutableList<AvatarData>,
dmUserIdentityState: IdentityState?, dmUserIdentityState: IdentityState?,
sharedHistoryIcon: SharedHistoryIcon, sharedHistoryIcon: SharedHistoryIcon,
dmUserStatus: DisplayedStatus?,
onRoomDetailsClick: () -> Unit, onRoomDetailsClick: () -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -91,6 +92,7 @@ internal fun MessagesViewTopBar(
roomAvatar = roomAvatar, roomAvatar = roomAvatar,
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
heroes = heroes, heroes = heroes,
dmUserStatus = dmUserStatus,
modifier = titleModifier modifier = titleModifier
) )
@@ -144,6 +146,7 @@ private fun RoomAvatarAndNameRow(
roomAvatar: AvatarData, roomAvatar: AvatarData,
heroes: ImmutableList<AvatarData>, heroes: ImmutableList<AvatarData>,
isTombstoned: Boolean, isTombstoned: Boolean,
dmUserStatus: DisplayedStatus?,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
@@ -157,14 +160,13 @@ private fun RoomAvatarAndNameRow(
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
), ),
) )
Text( DisplayNameWithStatus(
modifier = Modifier name = roomName ?: stringResource(CommonStrings.common_no_room_name),
.padding(start = 8.dp), status = dmUserStatus,
text = roomName ?: stringResource(CommonStrings.common_no_room_name), modifier = Modifier.padding(start = 8.dp),
style = ElementTheme.typography.fontBodyLgMedium, style = ElementTheme.typography.fontBodyLgMedium,
fontStyle = FontStyle.Italic.takeIf { roomName == null }, nameColor = ElementTheme.colors.textPrimary,
maxLines = 1, nameFontStyle = FontStyle.Italic.takeIf { roomName == null },
overflow = TextOverflow.Ellipsis
) )
} }
} }
@@ -184,6 +186,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
roomCallState: RoomCallState = RoomCallState.Unavailable, roomCallState: RoomCallState = RoomCallState.Unavailable,
dmUserIdentityState: IdentityState? = null, dmUserIdentityState: IdentityState? = null,
sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE, sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE,
dmUserStatus: DisplayedStatus? = null,
displayThreads: Boolean = false, displayThreads: Boolean = false,
) = MessagesViewTopBar( ) = MessagesViewTopBar(
roomName = roomName, roomName = roomName,
@@ -192,6 +195,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
heroes = heroes, heroes = heroes,
dmUserIdentityState = dmUserIdentityState, dmUserIdentityState = dmUserIdentityState,
sharedHistoryIcon = sharedHistoryIcon, sharedHistoryIcon = sharedHistoryIcon,
dmUserStatus = dmUserStatus,
onRoomDetailsClick = {}, onRoomDetailsClick = {},
onBackClick = {}, onBackClick = {},
menuActions = { menuActions = {
@@ -246,5 +250,11 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
AMessagesViewTopBar( AMessagesViewTopBar(
displayThreads = true, displayThreads = true,
) )
HorizontalDivider()
AMessagesViewTopBar(
roomName = "A DM with a very very very long name",
dmUserIdentityState = IdentityState.Verified,
dmUserStatus = DisplayedStatus.InCall(0L)
)
} }
} }
@@ -6,14 +6,6 @@
<string name="crypto_event_authenticity_unknown_device">"Зашыфравана невядомай ці выдаленай прыладай."</string> <string name="crypto_event_authenticity_unknown_device">"Зашыфравана невядомай ці выдаленай прыладай."</string>
<string name="crypto_event_authenticity_unsigned_device">"Зашыфравана прыладай, не пацверджанай яе ўладальнікам."</string> <string name="crypto_event_authenticity_unsigned_device">"Зашыфравана прыладай, не пацверджанай яе ўладальнікам."</string>
<string name="crypto_event_authenticity_unverified_identity">"Зашыфравана неправераным карыстальнікам."</string> <string name="crypto_event_authenticity_unverified_identity">"Зашыфравана неправераным карыстальнікам."</string>
<string name="emoji_picker_category_activity">"Актыўнасці"</string>
<string name="emoji_picker_category_flags">"Сцягі"</string>
<string name="emoji_picker_category_foods">"Ежа &amp; Напоі"</string>
<string name="emoji_picker_category_nature">"Жывёлы &amp; Прырода"</string>
<string name="emoji_picker_category_objects">"Аб\'екты"</string>
<string name="emoji_picker_category_people">"Усмешкі &amp; Удзельнікі"</string>
<string name="emoji_picker_category_places">"Падарожжы &amp; Месцы"</string>
<string name="emoji_picker_category_symbols">"Сімвалы"</string>
<string name="screen_media_upload_preview_error_failed_processing">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string> <string name="screen_media_upload_preview_error_failed_processing">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз."</string> <string name="screen_media_upload_preview_error_failed_sending">"Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз."</string>
<string name="screen_media_upload_preview_processing">"Апрацоўка…"</string> <string name="screen_media_upload_preview_processing">"Апрацоўка…"</string>
@@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_event_authenticity_sent_in_clear">"Без шифроване"</string> <string name="crypto_event_authenticity_sent_in_clear">"Без шифроване"</string>
<string name="emoji_picker_category_activity">"Дейности"</string>
<string name="emoji_picker_category_flags">"Знамена"</string>
<string name="emoji_picker_category_foods">"Храна &amp; Напитки"</string>
<string name="emoji_picker_category_nature">"Животни &amp; Природа"</string>
<string name="emoji_picker_category_objects">"Обекти"</string>
<string name="emoji_picker_category_people">"Усмивки &amp; Хора"</string>
<string name="emoji_picker_category_places">"Пътуване &amp; Места"</string>
<string name="emoji_picker_category_symbols">"Символи"</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Докоснете, за да промените качеството на качване на видео"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Докоснете, за да промените качеството на качване на видео"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Файлът не можа да бъде качен."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Файлът не можа да бъде качен."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Неуспешна обработка на мултимедия за качване, моля, опитайте отново."</string> <string name="screen_media_upload_preview_error_failed_processing">"Неуспешна обработка на мултимедия за качване, моля, опитайте отново."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Xifrat per un dispositiu desconegut o eliminat."</string> <string name="crypto_event_authenticity_unknown_device">"Xifrat per un dispositiu desconegut o eliminat."</string>
<string name="crypto_event_authenticity_unsigned_device">"Xifrat per un dispositiu no verificat pel seu propietari."</string> <string name="crypto_event_authenticity_unsigned_device">"Xifrat per un dispositiu no verificat pel seu propietari."</string>
<string name="crypto_event_authenticity_unverified_identity">"Xifrat per un usuari no verificat."</string> <string name="crypto_event_authenticity_unverified_identity">"Xifrat per un usuari no verificat."</string>
<string name="emoji_picker_category_activity">"Activitats"</string>
<string name="emoji_picker_category_flags">"Banderes"</string>
<string name="emoji_picker_category_foods">"Menjar &amp; begudes"</string>
<string name="emoji_picker_category_nature">"Animals i naturalesa"</string>
<string name="emoji_picker_category_objects">"Objectes"</string>
<string name="emoji_picker_category_people">"Cares i persones"</string>
<string name="emoji_picker_category_places">"Viatges i llocs"</string>
<string name="emoji_picker_category_recent">"Emoticones recents"</string>
<string name="emoji_picker_category_symbols">"Símbols"</string>
<string name="screen_media_upload_preview_caption_warning">"És possible que les llegendes no siguin visibles pels que utilitzin aplicacions antigues."</string> <string name="screen_media_upload_preview_caption_warning">"És possible que les llegendes no siguin visibles pels que utilitzin aplicacions antigues."</string>
<string name="screen_media_upload_preview_error_failed_processing">"No s\'ha pogut processar el contingut que s\'havia de pujar. Torna-ho a provar."</string> <string name="screen_media_upload_preview_error_failed_processing">"No s\'ha pogut processar el contingut que s\'havia de pujar. Torna-ho a provar."</string>
<string name="screen_media_upload_preview_error_failed_sending">"No s\'ha pogut pujar el contingut. Torna-ho a provar."</string> <string name="screen_media_upload_preview_error_failed_sending">"No s\'ha pogut pujar el contingut. Torna-ho a provar."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Šifrováno neznámým nebo smazaným zařízením."</string> <string name="crypto_event_authenticity_unknown_device">"Šifrováno neznámým nebo smazaným zařízením."</string>
<string name="crypto_event_authenticity_unsigned_device">"Šifrováno zařízením, které nebylo ověřeno jeho vlastníkem."</string> <string name="crypto_event_authenticity_unsigned_device">"Šifrováno zařízením, které nebylo ověřeno jeho vlastníkem."</string>
<string name="crypto_event_authenticity_unverified_identity">"Šifrováno neověřeným uživatelem."</string> <string name="crypto_event_authenticity_unverified_identity">"Šifrováno neověřeným uživatelem."</string>
<string name="emoji_picker_category_activity">"Aktivity"</string>
<string name="emoji_picker_category_flags">"Vlajky"</string>
<string name="emoji_picker_category_foods">"Jídlo a nápoje"</string>
<string name="emoji_picker_category_nature">"Zvířata a příroda"</string>
<string name="emoji_picker_category_objects">"Předměty"</string>
<string name="emoji_picker_category_people">"Smajlíci a lidé"</string>
<string name="emoji_picker_category_places">"Cestování a místa"</string>
<string name="emoji_picker_category_recent">"Nedávné emotikony"</string>
<string name="emoji_picker_category_symbols">"Symboly"</string>
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace."</string> <string name="screen_media_upload_preview_caption_warning">"Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Klepnutím změníte kvalitu nahrávání videa"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Klepnutím změníte kvalitu nahrávání videa"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Soubor nelze nahrát."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Soubor nelze nahrát."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Wedi\'i amgryptio gan ddyfais anhysbys neu wedi\'i dileu."</string> <string name="crypto_event_authenticity_unknown_device">"Wedi\'i amgryptio gan ddyfais anhysbys neu wedi\'i dileu."</string>
<string name="crypto_event_authenticity_unsigned_device">"Wedi\'i amgryptio gan ddyfais nad yw wedi\'i wirio gan ei pherchennog."</string> <string name="crypto_event_authenticity_unsigned_device">"Wedi\'i amgryptio gan ddyfais nad yw wedi\'i wirio gan ei pherchennog."</string>
<string name="crypto_event_authenticity_unverified_identity">"Wedi\'i amgryptio gan ddefnyddiwr heb ei wirio."</string> <string name="crypto_event_authenticity_unverified_identity">"Wedi\'i amgryptio gan ddefnyddiwr heb ei wirio."</string>
<string name="emoji_picker_category_activity">"Gweithgareddau"</string>
<string name="emoji_picker_category_flags">"Baneri"</string>
<string name="emoji_picker_category_foods">"Bwyd a Diod"</string>
<string name="emoji_picker_category_nature">"Anifeiliaid a Natur"</string>
<string name="emoji_picker_category_objects">"Gwrthrychau"</string>
<string name="emoji_picker_category_people">"Wynebau Hapus a Phobl"</string>
<string name="emoji_picker_category_places">"Teithio a Llefydd"</string>
<string name="emoji_picker_category_recent">"Emojis diweddar"</string>
<string name="emoji_picker_category_symbols">"Symbolau"</string>
<string name="screen_media_upload_preview_caption_warning">"Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn."</string> <string name="screen_media_upload_preview_caption_warning">"Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tapiwch i newid ansawdd llwytho\'r fideo"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Tapiwch i newid ansawdd llwytho\'r fideo"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Nid oedd modd llwytho\'r ffeil."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Nid oedd modd llwytho\'r ffeil."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Krypteret af en ukendt eller slettet enhed."</string> <string name="crypto_event_authenticity_unknown_device">"Krypteret af en ukendt eller slettet enhed."</string>
<string name="crypto_event_authenticity_unsigned_device">"Krypteret af en enhed, der ikke er verificeret af sin ejer."</string> <string name="crypto_event_authenticity_unsigned_device">"Krypteret af en enhed, der ikke er verificeret af sin ejer."</string>
<string name="crypto_event_authenticity_unverified_identity">"Krypteret af en ikke-verificeret bruger."</string> <string name="crypto_event_authenticity_unverified_identity">"Krypteret af en ikke-verificeret bruger."</string>
<string name="emoji_picker_category_activity">"Aktiviteter"</string>
<string name="emoji_picker_category_flags">"Flag"</string>
<string name="emoji_picker_category_foods">"Mad og drikke"</string>
<string name="emoji_picker_category_nature">"Dyr og natur"</string>
<string name="emoji_picker_category_objects">"Objekter"</string>
<string name="emoji_picker_category_people">"Smileys og personer"</string>
<string name="emoji_picker_category_places">"Rejser og steder"</string>
<string name="emoji_picker_category_recent">"Seneste emojis"</string>
<string name="emoji_picker_category_symbols">"Symboler"</string>
<string name="screen_media_upload_preview_caption_warning">"Billedtekster er muligvis ikke synlige for personer, der bruger ældre apps."</string> <string name="screen_media_upload_preview_caption_warning">"Billedtekster er muligvis ikke synlige for personer, der bruger ældre apps."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tryk for at ændre videokvaliteten i uploadet"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Tryk for at ændre videokvaliteten i uploadet"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Filen kunne ikke uploades."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Filen kunne ikke uploades."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Verschlüsselt von einem unbekannten oder gelöschten Gerät."</string> <string name="crypto_event_authenticity_unknown_device">"Verschlüsselt von einem unbekannten oder gelöschten Gerät."</string>
<string name="crypto_event_authenticity_unsigned_device">"Verschlüsselt durch ein Gerät, das nicht von seinem Besitzer verifiziert wurde."</string> <string name="crypto_event_authenticity_unsigned_device">"Verschlüsselt durch ein Gerät, das nicht von seinem Besitzer verifiziert wurde."</string>
<string name="crypto_event_authenticity_unverified_identity">"Verschlüsselt durch einen nicht verifizierten Nutzer."</string> <string name="crypto_event_authenticity_unverified_identity">"Verschlüsselt durch einen nicht verifizierten Nutzer."</string>
<string name="emoji_picker_category_activity">"Aktivitäten"</string>
<string name="emoji_picker_category_flags">"Flaggen"</string>
<string name="emoji_picker_category_foods">"Essen &amp; Trinken"</string>
<string name="emoji_picker_category_nature">"Tiere &amp; Natur"</string>
<string name="emoji_picker_category_objects">"Objekte"</string>
<string name="emoji_picker_category_people">"Smileys &amp; Menschen"</string>
<string name="emoji_picker_category_places">"Reisen &amp; Orte"</string>
<string name="emoji_picker_category_recent">"Zuletzt verwendete Emojis"</string>
<string name="emoji_picker_category_symbols">"Symbole"</string>
<string name="screen_media_upload_preview_caption_warning">"Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar."</string> <string name="screen_media_upload_preview_caption_warning">"Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tippe, um die Qualität des Video-Uploads zu ändern"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Tippe, um die Qualität des Video-Uploads zu ändern"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Die Datei konnte nicht hochgeladen werden."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Die Datei konnte nicht hochgeladen werden."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Κρυπτογραφημένο από άγνωστη ή διαγεγραμμένη συσκευή."</string> <string name="crypto_event_authenticity_unknown_device">"Κρυπτογραφημένο από άγνωστη ή διαγεγραμμένη συσκευή."</string>
<string name="crypto_event_authenticity_unsigned_device">"Κρυπτογραφημένο από μια συσκευή που δεν έχει επαληθευτεί από τον ιδιοκτήτη της."</string> <string name="crypto_event_authenticity_unsigned_device">"Κρυπτογραφημένο από μια συσκευή που δεν έχει επαληθευτεί από τον ιδιοκτήτη της."</string>
<string name="crypto_event_authenticity_unverified_identity">"Κρυπτογραφημένο από μη επαληθευμένο χρήστη."</string> <string name="crypto_event_authenticity_unverified_identity">"Κρυπτογραφημένο από μη επαληθευμένο χρήστη."</string>
<string name="emoji_picker_category_activity">"Δραστηριότητες"</string>
<string name="emoji_picker_category_flags">"Σημαίες"</string>
<string name="emoji_picker_category_foods">"Φαγητό &amp; Ποτό"</string>
<string name="emoji_picker_category_nature">"Ζώα &amp; Φύση"</string>
<string name="emoji_picker_category_objects">"Αντικείμενα"</string>
<string name="emoji_picker_category_people">"Φατσούλες &amp; Άνθρωποι"</string>
<string name="emoji_picker_category_places">"Ταξίδια &amp; Μέρη"</string>
<string name="emoji_picker_category_recent">"Πρόσφατα emoji"</string>
<string name="emoji_picker_category_symbols">"Σύμβολα"</string>
<string name="screen_media_upload_preview_caption_warning">"Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές."</string> <string name="screen_media_upload_preview_caption_warning">"Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Πατήστε για να αλλάξετε την ποιότητα μεταφόρτωσης βίντεο"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Πατήστε για να αλλάξετε την ποιότητα μεταφόρτωσης βίντεο"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Δεν ήταν δυνατή η μεταφόρτωση του αρχείου."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Δεν ήταν δυνατή η μεταφόρτωση του αρχείου."</string>
@@ -7,14 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Cifrado por un dispositivo desconocido o eliminado."</string> <string name="crypto_event_authenticity_unknown_device">"Cifrado por un dispositivo desconocido o eliminado."</string>
<string name="crypto_event_authenticity_unsigned_device">"Cifrado por un dispositivo no verificado por su propietario."</string> <string name="crypto_event_authenticity_unsigned_device">"Cifrado por un dispositivo no verificado por su propietario."</string>
<string name="crypto_event_authenticity_unverified_identity">"Cifrado por un usuario no verificado."</string> <string name="crypto_event_authenticity_unverified_identity">"Cifrado por un usuario no verificado."</string>
<string name="emoji_picker_category_activity">"Actividades"</string>
<string name="emoji_picker_category_flags">"Banderas"</string>
<string name="emoji_picker_category_foods">"Comida y bebida"</string>
<string name="emoji_picker_category_nature">"Animales y naturaleza"</string>
<string name="emoji_picker_category_objects">"Objetos"</string>
<string name="emoji_picker_category_people">"Emojis y personas"</string>
<string name="emoji_picker_category_places">"Viajes y lugares"</string>
<string name="emoji_picker_category_symbols">"Símbolos"</string>
<string name="screen_media_upload_preview_caption_warning">"Es posible que las leyendas no sean visibles para las personas que usan aplicaciones más antiguas."</string> <string name="screen_media_upload_preview_caption_warning">"Es posible que las leyendas no sean visibles para las personas que usan aplicaciones más antiguas."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Error al procesar el contenido multimedia, por favor inténtalo de nuevo."</string> <string name="screen_media_upload_preview_error_failed_processing">"Error al procesar el contenido multimedia, por favor inténtalo de nuevo."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Error al subir el contenido multimedia, por favor inténtalo de nuevo."</string> <string name="screen_media_upload_preview_error_failed_sending">"Error al subir el contenido multimedia, por favor inténtalo de nuevo."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Krüptitud tundmatu või kustutatud seadme poolt."</string> <string name="crypto_event_authenticity_unknown_device">"Krüptitud tundmatu või kustutatud seadme poolt."</string>
<string name="crypto_event_authenticity_unsigned_device">"Krüptitud seadme poolt, mida tema omanik pole verifitseerinud."</string> <string name="crypto_event_authenticity_unsigned_device">"Krüptitud seadme poolt, mida tema omanik pole verifitseerinud."</string>
<string name="crypto_event_authenticity_unverified_identity">"Krüptitud verifitseerimata kasutaja poolt."</string> <string name="crypto_event_authenticity_unverified_identity">"Krüptitud verifitseerimata kasutaja poolt."</string>
<string name="emoji_picker_category_activity">"Tegevused"</string>
<string name="emoji_picker_category_flags">"Lipud"</string>
<string name="emoji_picker_category_foods">"Toit ja jook"</string>
<string name="emoji_picker_category_nature">"Loomad ja loodus"</string>
<string name="emoji_picker_category_objects">"Esemed"</string>
<string name="emoji_picker_category_people">"Emotikonid ja inimesed"</string>
<string name="emoji_picker_category_places">"Reisimine ja kohad"</string>
<string name="emoji_picker_category_recent">"Hiljutised emojid"</string>
<string name="emoji_picker_category_symbols">"Sümbolid"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Pööra pilti ümber rõhttelje"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Pööra pilti ümber rõhttelje"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Pilt on pööratud ümber rõhttelje"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Pilt on pööratud ümber rõhttelje"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Pildi algne variant"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Pildi algne variant"</string>
@@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crypto_event_authenticity_sent_in_clear">"Ez dago zifratuta."</string> <string name="crypto_event_authenticity_sent_in_clear">"Ez dago zifratuta."</string>
<string name="emoji_picker_category_activity">"Jarduerak"</string>
<string name="emoji_picker_category_flags">"Banderak"</string>
<string name="emoji_picker_category_foods">"Jana eta edana"</string>
<string name="emoji_picker_category_nature">"Animaliak eta natura"</string>
<string name="emoji_picker_category_objects">"Objektuak"</string>
<string name="emoji_picker_category_people">"Irribartxoak eta jendea"</string>
<string name="emoji_picker_category_places">"Bidaiak eta tokiak"</string>
<string name="emoji_picker_category_symbols">"Ikurrak"</string>
<string name="screen_media_upload_preview_error_failed_sending">"Huts egin du multimedia igotzeak, saiatu berriro."</string> <string name="screen_media_upload_preview_error_failed_sending">"Huts egin du multimedia igotzeak, saiatu berriro."</string>
<string name="screen_report_content_block_user">"Blokeatu erabiltzailea"</string> <string name="screen_report_content_block_user">"Blokeatu erabiltzailea"</string>
<string name="screen_report_content_explanation">"Mezua zure zerbitzariko administratzaileari jakinaraziko zaio. Ezingo dute zifratutako mezurik irakurri."</string> <string name="screen_report_content_explanation">"Mezua zure zerbitzariko administratzaileari jakinaraziko zaio. Ezingo dute zifratutako mezurik irakurri."</string>
@@ -6,15 +6,6 @@
<string name="crypto_event_authenticity_unknown_device">"رمز شده به دست افزاره‌ای ناشناخته یا حذف شده."</string> <string name="crypto_event_authenticity_unknown_device">"رمز شده به دست افزاره‌ای ناشناخته یا حذف شده."</string>
<string name="crypto_event_authenticity_unsigned_device">"رمز شده به دست افزاره‌ای که از سوی مالکش تأیید نشده."</string> <string name="crypto_event_authenticity_unsigned_device">"رمز شده به دست افزاره‌ای که از سوی مالکش تأیید نشده."</string>
<string name="crypto_event_authenticity_unverified_identity">"رمز شده به دست کاربری تأیید نشده."</string> <string name="crypto_event_authenticity_unverified_identity">"رمز شده به دست کاربری تأیید نشده."</string>
<string name="emoji_picker_category_activity">"فعّالیت‌ها"</string>
<string name="emoji_picker_category_flags">"پرچم‌ها"</string>
<string name="emoji_picker_category_foods">"غذا و نوشیدنی"</string>
<string name="emoji_picker_category_nature">"حیوانات و طبعیت"</string>
<string name="emoji_picker_category_objects">"اشیا"</string>
<string name="emoji_picker_category_people">"شکلک‌ها و افراد"</string>
<string name="emoji_picker_category_places">"سفر و مکان‌ها"</string>
<string name="emoji_picker_category_recent">"شکلک‌های اخیر"</string>
<string name="emoji_picker_category_symbols">"نمادها"</string>
<string name="screen_media_upload_preview_error_failed_processing">"پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."</string> <string name="screen_media_upload_preview_error_failed_processing">"پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_media_upload_preview_error_failed_sending">"بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید."</string> <string name="screen_media_upload_preview_error_failed_sending">"بارگذاری رسانه شکست خورد. لطفاً دوباره تلاش کنید."</string>
<string name="screen_media_upload_preview_processing">"پردازش کردن…"</string> <string name="screen_media_upload_preview_processing">"پردازش کردن…"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Tuntemattoman tai poistetun laitteen salaama."</string> <string name="crypto_event_authenticity_unknown_device">"Tuntemattoman tai poistetun laitteen salaama."</string>
<string name="crypto_event_authenticity_unsigned_device">"Salattu laitteella, jota sen omistaja ei ole vahvistanut."</string> <string name="crypto_event_authenticity_unsigned_device">"Salattu laitteella, jota sen omistaja ei ole vahvistanut."</string>
<string name="crypto_event_authenticity_unverified_identity">"Vahvistamattoman käyttäjän salaama."</string> <string name="crypto_event_authenticity_unverified_identity">"Vahvistamattoman käyttäjän salaama."</string>
<string name="emoji_picker_category_activity">"Aktiviteetit"</string>
<string name="emoji_picker_category_flags">"Liput"</string>
<string name="emoji_picker_category_foods">"Ruoka ja juoma"</string>
<string name="emoji_picker_category_nature">"Eläimet ja luonto"</string>
<string name="emoji_picker_category_objects">"Esineet"</string>
<string name="emoji_picker_category_people">"Hymiöt ja ihmiset"</string>
<string name="emoji_picker_category_places">"Matkustaminen ja paikat"</string>
<string name="emoji_picker_category_recent">"Viimeaikaiset emojit"</string>
<string name="emoji_picker_category_symbols">"Symbolit"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Käännä kuva vaakasuunnassa"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Käännä kuva vaakasuunnassa"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Käännetty vaakasuunnassa"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Käännetty vaakasuunnassa"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Alkuperäinen"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Alkuperäinen"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Chiffré par un appareil inconnu ou supprimé."</string> <string name="crypto_event_authenticity_unknown_device">"Chiffré par un appareil inconnu ou supprimé."</string>
<string name="crypto_event_authenticity_unsigned_device">"Chiffré par un appareil non vérifié par son propriétaire."</string> <string name="crypto_event_authenticity_unsigned_device">"Chiffré par un appareil non vérifié par son propriétaire."</string>
<string name="crypto_event_authenticity_unverified_identity">"Chiffré par un utilisateur non vérifié."</string> <string name="crypto_event_authenticity_unverified_identity">"Chiffré par un utilisateur non vérifié."</string>
<string name="emoji_picker_category_activity">"Activités"</string>
<string name="emoji_picker_category_flags">"Drapeaux"</string>
<string name="emoji_picker_category_foods">"Nourriture et boissons"</string>
<string name="emoji_picker_category_nature">"Animaux et nature"</string>
<string name="emoji_picker_category_objects">"Objets"</string>
<string name="emoji_picker_category_people">"Émoticônes et personnes"</string>
<string name="emoji_picker_category_places">"Voyages &amp; lieux"</string>
<string name="emoji_picker_category_recent">"Emojis récents"</string>
<string name="emoji_picker_category_symbols">"Symboles"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Retourner l’image horizontalement"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Retourner l’image horizontalement"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Retournée horizontalement"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Retournée horizontalement"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Originale"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Originale"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Šifrirano nepoznatim ili izbrisanim uređajem."</string> <string name="crypto_event_authenticity_unknown_device">"Šifrirano nepoznatim ili izbrisanim uređajem."</string>
<string name="crypto_event_authenticity_unsigned_device">"Šifrirano uređajem koji vlasnik nije potvrdio."</string> <string name="crypto_event_authenticity_unsigned_device">"Šifrirano uređajem koji vlasnik nije potvrdio."</string>
<string name="crypto_event_authenticity_unverified_identity">"Šifrirao neprovjereni korisnik."</string> <string name="crypto_event_authenticity_unverified_identity">"Šifrirao neprovjereni korisnik."</string>
<string name="emoji_picker_category_activity">"Aktivnosti"</string>
<string name="emoji_picker_category_flags">"Zastave"</string>
<string name="emoji_picker_category_foods">"Hrana i piće"</string>
<string name="emoji_picker_category_nature">"Životinje i priroda"</string>
<string name="emoji_picker_category_objects">"Objekti"</string>
<string name="emoji_picker_category_people">"Emotikoni i osobe"</string>
<string name="emoji_picker_category_places">"Putovanja i mjesta"</string>
<string name="emoji_picker_category_recent">"Nedavni emotikoni"</string>
<string name="emoji_picker_category_symbols">"Simboli"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Okreni sliku vodoravno"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Okreni sliku vodoravno"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Okrenuto vodoravno"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Okrenuto vodoravno"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Izvornik"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Izvornik"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Ismeretlen vagy törölt eszköz által titkosítva."</string> <string name="crypto_event_authenticity_unknown_device">"Ismeretlen vagy törölt eszköz által titkosítva."</string>
<string name="crypto_event_authenticity_unsigned_device">"A tulajdonos által nem ellenőrzött eszköz által titkosítva."</string> <string name="crypto_event_authenticity_unsigned_device">"A tulajdonos által nem ellenőrzött eszköz által titkosítva."</string>
<string name="crypto_event_authenticity_unverified_identity">"Nem ellenőrzött felhasználó által titkosítva."</string> <string name="crypto_event_authenticity_unverified_identity">"Nem ellenőrzött felhasználó által titkosítva."</string>
<string name="emoji_picker_category_activity">"Tevékenységek"</string>
<string name="emoji_picker_category_flags">"Zászlók"</string>
<string name="emoji_picker_category_foods">"Étel és ital"</string>
<string name="emoji_picker_category_nature">"Állatok és természet"</string>
<string name="emoji_picker_category_objects">"Tárgyak"</string>
<string name="emoji_picker_category_people">"Mosolyok és emberek"</string>
<string name="emoji_picker_category_places">"Utazás és helyek"</string>
<string name="emoji_picker_category_recent">"Legutóbbi emodzsik"</string>
<string name="emoji_picker_category_symbols">"Szimbólumok"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Kép vízszintes tükrözése"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Kép vízszintes tükrözése"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Vízszintesen tükrözve"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Vízszintesen tükrözve"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Eredeti"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Eredeti"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Dienkripsi oleh perangkat yang tidak dikenal atau dihapus."</string> <string name="crypto_event_authenticity_unknown_device">"Dienkripsi oleh perangkat yang tidak dikenal atau dihapus."</string>
<string name="crypto_event_authenticity_unsigned_device">"Dienkripsi oleh perangkat yang tidak diverifikasi oleh pemiliknya."</string> <string name="crypto_event_authenticity_unsigned_device">"Dienkripsi oleh perangkat yang tidak diverifikasi oleh pemiliknya."</string>
<string name="crypto_event_authenticity_unverified_identity">"Dienkripsi oleh pengguna yang tidak terverifikasi."</string> <string name="crypto_event_authenticity_unverified_identity">"Dienkripsi oleh pengguna yang tidak terverifikasi."</string>
<string name="emoji_picker_category_activity">"Aktivitas"</string>
<string name="emoji_picker_category_flags">"Bendera"</string>
<string name="emoji_picker_category_foods">"Makanan &amp; Minuman"</string>
<string name="emoji_picker_category_nature">"Hewan &amp; Alam"</string>
<string name="emoji_picker_category_objects">"Objek"</string>
<string name="emoji_picker_category_people">"Senyuman &amp; Orang"</string>
<string name="emoji_picker_category_places">"Wisata &amp; Tempat"</string>
<string name="emoji_picker_category_recent">"Emojis Sebelumnya"</string>
<string name="emoji_picker_category_symbols">"Simbol"</string>
<string name="screen_image_edition_a11y_rotate_to_the_left">"Putar gambar ke kiri"</string> <string name="screen_image_edition_a11y_rotate_to_the_left">"Putar gambar ke kiri"</string>
<plurals name="screen_image_edition_a11y_rotation_state"> <plurals name="screen_image_edition_a11y_rotation_state">
<item quantity="other">"%1$d derajat"</item> <item quantity="other">"%1$d derajat"</item>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Cifrato da un dispositivo sconosciuto o eliminato."</string> <string name="crypto_event_authenticity_unknown_device">"Cifrato da un dispositivo sconosciuto o eliminato."</string>
<string name="crypto_event_authenticity_unsigned_device">"Cifrato da un dispositivo non verificato dal proprietario."</string> <string name="crypto_event_authenticity_unsigned_device">"Cifrato da un dispositivo non verificato dal proprietario."</string>
<string name="crypto_event_authenticity_unverified_identity">"Cifrato da un utente non verificato."</string> <string name="crypto_event_authenticity_unverified_identity">"Cifrato da un utente non verificato."</string>
<string name="emoji_picker_category_activity">"Attività"</string>
<string name="emoji_picker_category_flags">"Bandiere"</string>
<string name="emoji_picker_category_foods">"Cibi &amp; Bevande"</string>
<string name="emoji_picker_category_nature">"Animali &amp; Natura"</string>
<string name="emoji_picker_category_objects">"Oggetti"</string>
<string name="emoji_picker_category_people">"Faccine &amp; Persone"</string>
<string name="emoji_picker_category_places">"Viaggi &amp; Luoghi"</string>
<string name="emoji_picker_category_recent">"Emoji recenti"</string>
<string name="emoji_picker_category_symbols">"Simboli"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Capovolgi l\'immagine orizzontalmente"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Capovolgi l\'immagine orizzontalmente"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Capovolto orizzontalmente"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Capovolto orizzontalmente"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Originale"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Originale"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"削除されたまたは不明な端末により暗号化されています。"</string> <string name="crypto_event_authenticity_unknown_device">"削除されたまたは不明な端末により暗号化されています。"</string>
<string name="crypto_event_authenticity_unsigned_device">"所有者に検証されていない端末により暗号化されています。"</string> <string name="crypto_event_authenticity_unsigned_device">"所有者に検証されていない端末により暗号化されています。"</string>
<string name="crypto_event_authenticity_unverified_identity">"未検証のユーザーにより暗号化されています。"</string> <string name="crypto_event_authenticity_unverified_identity">"未検証のユーザーにより暗号化されています。"</string>
<string name="emoji_picker_category_activity">"アクティビティ"</string>
<string name="emoji_picker_category_flags">"旗"</string>
<string name="emoji_picker_category_foods">"食べ物"</string>
<string name="emoji_picker_category_nature">"動物・自然"</string>
<string name="emoji_picker_category_objects">"物"</string>
<string name="emoji_picker_category_people">"顔・人"</string>
<string name="emoji_picker_category_places">"旅・場所"</string>
<string name="emoji_picker_category_recent">"最近使用"</string>
<string name="emoji_picker_category_symbols">"記号"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"水平に反転"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"水平に反転"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"垂直に反転"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"垂直に反転"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"元画像"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"元画像"</string>
@@ -1,13 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"აქტივობები"</string>
<string name="emoji_picker_category_flags">"დროშები"</string>
<string name="emoji_picker_category_foods">"Საჭმელ-სასმელი"</string>
<string name="emoji_picker_category_nature">"ცხოველები &amp; ბუნება"</string>
<string name="emoji_picker_category_objects">"ობიექტები"</string>
<string name="emoji_picker_category_people">"ღიმილები &amp; ხალხი"</string>
<string name="emoji_picker_category_places">"მოგზაურობა და ადგილები"</string>
<string name="emoji_picker_category_symbols">"სიმბოლოები"</string>
<string name="screen_media_upload_preview_error_failed_processing">"მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა."</string> <string name="screen_media_upload_preview_error_failed_processing">"მედიის ატვირთვა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა."</string>
<string name="screen_media_upload_preview_error_failed_sending">"მედიის ატვირთვა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."</string> <string name="screen_media_upload_preview_error_failed_sending">"მედიის ატვირთვა ვერ მოხერხდა, გთხოვთ, სცადოთ ხელახლა."</string>
<string name="screen_report_content_block_user">"მომხმარებლის დაბლოკვა"</string> <string name="screen_report_content_block_user">"მომხმარებლის დაბლოკვა"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"알 수 없거나 삭제된 장치에 의해 암호화됩니다."</string> <string name="crypto_event_authenticity_unknown_device">"알 수 없거나 삭제된 장치에 의해 암호화됩니다."</string>
<string name="crypto_event_authenticity_unsigned_device">"소유자가 확인하지 않은 장치에 의해 암호화되었습니다."</string> <string name="crypto_event_authenticity_unsigned_device">"소유자가 확인하지 않은 장치에 의해 암호화되었습니다."</string>
<string name="crypto_event_authenticity_unverified_identity">"검증되지 않은 사용자에 의해 암호화되었습니다."</string> <string name="crypto_event_authenticity_unverified_identity">"검증되지 않은 사용자에 의해 암호화되었습니다."</string>
<string name="emoji_picker_category_activity">"활동"</string>
<string name="emoji_picker_category_flags">"깃발"</string>
<string name="emoji_picker_category_foods">"음식 &amp; 음료"</string>
<string name="emoji_picker_category_nature">"동물 &amp; 자연"</string>
<string name="emoji_picker_category_objects">"사물"</string>
<string name="emoji_picker_category_people">"표정 &amp; 사람"</string>
<string name="emoji_picker_category_places">"여행 &amp; 장소"</string>
<string name="emoji_picker_category_recent">"최근 이모지"</string>
<string name="emoji_picker_category_symbols">"상징"</string>
<string name="screen_image_edition_a11y_rotate_to_the_left">"이미지를 왼쪽으로 회전"</string> <string name="screen_image_edition_a11y_rotate_to_the_left">"이미지를 왼쪽으로 회전"</string>
<plurals name="screen_image_edition_a11y_rotation_state"> <plurals name="screen_image_edition_a11y_rotation_state">
<item quantity="other">"%1$d도"</item> <item quantity="other">"%1$d도"</item>
@@ -1,13 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="emoji_picker_category_activity">"Veikla"</string>
<string name="emoji_picker_category_flags">"Vėliavos"</string>
<string name="emoji_picker_category_foods">"Maistas ir Gėrimai"</string>
<string name="emoji_picker_category_nature">"Gyvūnai ir Gamta"</string>
<string name="emoji_picker_category_objects">"Objektai"</string>
<string name="emoji_picker_category_people">"Šypsenėlės ir Žmonės"</string>
<string name="emoji_picker_category_places">"Kelionės ir Vietovės"</string>
<string name="emoji_picker_category_symbols">"Simboliai"</string>
<string name="screen_media_upload_preview_error_failed_processing">"Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą."</string> <string name="screen_media_upload_preview_error_failed_processing">"Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Nepavyko įkelti laikmenos, pabandykite dar kartą."</string> <string name="screen_media_upload_preview_error_failed_sending">"Nepavyko įkelti laikmenos, pabandykite dar kartą."</string>
<string name="screen_report_content_block_user">"Blokuoti vartotoją"</string> <string name="screen_report_content_block_user">"Blokuoti vartotoją"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Kryptert av en ukjent eller slettet enhet."</string> <string name="crypto_event_authenticity_unknown_device">"Kryptert av en ukjent eller slettet enhet."</string>
<string name="crypto_event_authenticity_unsigned_device">"Kryptert med en enhet som ikke er verifisert av eieren."</string> <string name="crypto_event_authenticity_unsigned_device">"Kryptert med en enhet som ikke er verifisert av eieren."</string>
<string name="crypto_event_authenticity_unverified_identity">"Kryptert av en ubekreftet bruker."</string> <string name="crypto_event_authenticity_unverified_identity">"Kryptert av en ubekreftet bruker."</string>
<string name="emoji_picker_category_activity">"Aktiviteter"</string>
<string name="emoji_picker_category_flags">"Flagg"</string>
<string name="emoji_picker_category_foods">"Mat og drikke"</string>
<string name="emoji_picker_category_nature">"Dyr og natur"</string>
<string name="emoji_picker_category_objects">"Gjenstander"</string>
<string name="emoji_picker_category_people">"Smilefjes og mennesker"</string>
<string name="emoji_picker_category_places">"Reising og steder"</string>
<string name="emoji_picker_category_recent">"Nylige emojier"</string>
<string name="emoji_picker_category_symbols">"Symboler"</string>
<string name="screen_media_upload_preview_caption_warning">"Teksting er kanskje ikke synlig for personer som bruker eldre apper."</string> <string name="screen_media_upload_preview_caption_warning">"Teksting er kanskje ikke synlig for personer som bruker eldre apper."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Trykk for å endre kvaliteten på videoopplastingen"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Trykk for å endre kvaliteten på videoopplastingen"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Filen kunne ikke lastes opp."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Filen kunne ikke lastes opp."</string>
@@ -6,14 +6,6 @@
<string name="crypto_event_authenticity_unknown_device">"Versleuteld door een onbekend of verwijderd apparaat."</string> <string name="crypto_event_authenticity_unknown_device">"Versleuteld door een onbekend of verwijderd apparaat."</string>
<string name="crypto_event_authenticity_unsigned_device">"Versleuteld door een apparaat dat niet is geverifieerd door de eigenaar."</string> <string name="crypto_event_authenticity_unsigned_device">"Versleuteld door een apparaat dat niet is geverifieerd door de eigenaar."</string>
<string name="crypto_event_authenticity_unverified_identity">"Versleuteld door een niet-geverifieerde gebruiker."</string> <string name="crypto_event_authenticity_unverified_identity">"Versleuteld door een niet-geverifieerde gebruiker."</string>
<string name="emoji_picker_category_activity">"Activiteiten"</string>
<string name="emoji_picker_category_flags">"Vlaggen"</string>
<string name="emoji_picker_category_foods">"Eten &amp; Drinken"</string>
<string name="emoji_picker_category_nature">"Dieren &amp; Natuur"</string>
<string name="emoji_picker_category_objects">"Voorwerpen"</string>
<string name="emoji_picker_category_people">"Smileys &amp; Mensen"</string>
<string name="emoji_picker_category_places">"Reizen &amp; Locaties"</string>
<string name="emoji_picker_category_symbols">"Symbolen"</string>
<string name="screen_media_upload_preview_error_failed_processing">"Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw."</string> <string name="screen_media_upload_preview_error_failed_processing">"Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Het uploaden van media is mislukt. Probeer het opnieuw."</string> <string name="screen_media_upload_preview_error_failed_sending">"Het uploaden van media is mislukt. Probeer het opnieuw."</string>
<string name="screen_report_content_block_user">"Gebruiker blokkeren"</string> <string name="screen_report_content_block_user">"Gebruiker blokkeren"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Zaszyfrowana przez nieznane lub usunięte urządzenie."</string> <string name="crypto_event_authenticity_unknown_device">"Zaszyfrowana przez nieznane lub usunięte urządzenie."</string>
<string name="crypto_event_authenticity_unsigned_device">"Zaszyfrowana przez urządzenie niezweryfikowane przez jego właściciela."</string> <string name="crypto_event_authenticity_unsigned_device">"Zaszyfrowana przez urządzenie niezweryfikowane przez jego właściciela."</string>
<string name="crypto_event_authenticity_unverified_identity">"Zaszyfrowana przez niezweryfikowanego użytkownika."</string> <string name="crypto_event_authenticity_unverified_identity">"Zaszyfrowana przez niezweryfikowanego użytkownika."</string>
<string name="emoji_picker_category_activity">"Aktywności"</string>
<string name="emoji_picker_category_flags">"Flagi"</string>
<string name="emoji_picker_category_foods">"Jedzenie i napoje"</string>
<string name="emoji_picker_category_nature">"Zwierzęta i natura"</string>
<string name="emoji_picker_category_objects">"Obiekty"</string>
<string name="emoji_picker_category_people">"Buźki i osoby"</string>
<string name="emoji_picker_category_places">"Podróż i miejsca"</string>
<string name="emoji_picker_category_recent">"Ostatnie emoji"</string>
<string name="emoji_picker_category_symbols">"Symbole"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Odwróć obraz w poziomie"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Odwróć obraz w poziomie"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Odwrócono w poziomie"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Odwrócono w poziomie"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Oryginalny"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Oryginalny"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Criptografado por um dispositivo desconhecido ou apagado."</string> <string name="crypto_event_authenticity_unknown_device">"Criptografado por um dispositivo desconhecido ou apagado."</string>
<string name="crypto_event_authenticity_unsigned_device">"Criptografado por um dispositivo que não foi verificado pelo seu dono."</string> <string name="crypto_event_authenticity_unsigned_device">"Criptografado por um dispositivo que não foi verificado pelo seu dono."</string>
<string name="crypto_event_authenticity_unverified_identity">"Criptografado por um usuário não verificado."</string> <string name="crypto_event_authenticity_unverified_identity">"Criptografado por um usuário não verificado."</string>
<string name="emoji_picker_category_activity">"Atividades"</string>
<string name="emoji_picker_category_flags">"Bandeiras"</string>
<string name="emoji_picker_category_foods">"Comida &amp; Bebida"</string>
<string name="emoji_picker_category_nature">"Animais &amp; Natureza"</string>
<string name="emoji_picker_category_objects">"Objetos"</string>
<string name="emoji_picker_category_people">"Sorrisos &amp; Pessoas"</string>
<string name="emoji_picker_category_places">"Viagens &amp; Lugares"</string>
<string name="emoji_picker_category_recent">"Emojis recentes"</string>
<string name="emoji_picker_category_symbols">"Símbolos"</string>
<string name="screen_media_upload_preview_caption_warning">"As legendas podem não ser visíveis para pessoas que usam apps mais antigos."</string> <string name="screen_media_upload_preview_caption_warning">"As legendas podem não ser visíveis para pessoas que usam apps mais antigos."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Toque para alterar a qualidade do envio do vídeo"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Toque para alterar a qualidade do envio do vídeo"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"O arquivo não pôde ser enviado."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"O arquivo não pôde ser enviado."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Cifragem com origem num dispositivo eliminado ou desconhecido."</string> <string name="crypto_event_authenticity_unknown_device">"Cifragem com origem num dispositivo eliminado ou desconhecido."</string>
<string name="crypto_event_authenticity_unsigned_device">"Cifragem com origem num dispositivo não verificado pelo seu dono."</string> <string name="crypto_event_authenticity_unsigned_device">"Cifragem com origem num dispositivo não verificado pelo seu dono."</string>
<string name="crypto_event_authenticity_unverified_identity">"Cifragem com origem num utilizador não verificado."</string> <string name="crypto_event_authenticity_unverified_identity">"Cifragem com origem num utilizador não verificado."</string>
<string name="emoji_picker_category_activity">"Atividades"</string>
<string name="emoji_picker_category_flags">"Bandeiras"</string>
<string name="emoji_picker_category_foods">"Comidas e Bebidas"</string>
<string name="emoji_picker_category_nature">"Animais e Natureza"</string>
<string name="emoji_picker_category_objects">"Objetos"</string>
<string name="emoji_picker_category_people">"Caras e Pessoas"</string>
<string name="emoji_picker_category_places">"Viagens e Lugares"</string>
<string name="emoji_picker_category_recent">"Emojis recentes"</string>
<string name="emoji_picker_category_symbols">"Símbolos"</string>
<string name="screen_media_upload_preview_caption_warning">"As legendas poderão não ser visíveis em versões mais antigas da aplicação."</string> <string name="screen_media_upload_preview_caption_warning">"As legendas poderão não ser visíveis em versões mais antigas da aplicação."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Toca para alterar a qualidade de carregamento do vídeo"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Toca para alterar a qualidade de carregamento do vídeo"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Não foi possível enviar o ficheiro"</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Não foi possível enviar o ficheiro"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Criptat de un dispozitiv necunoscut sau șters."</string> <string name="crypto_event_authenticity_unknown_device">"Criptat de un dispozitiv necunoscut sau șters."</string>
<string name="crypto_event_authenticity_unsigned_device">"Criptat de un dispozitiv care nu este verificat de proprietarul său."</string> <string name="crypto_event_authenticity_unsigned_device">"Criptat de un dispozitiv care nu este verificat de proprietarul său."</string>
<string name="crypto_event_authenticity_unverified_identity">"Criptat de un utilizator neverificat."</string> <string name="crypto_event_authenticity_unverified_identity">"Criptat de un utilizator neverificat."</string>
<string name="emoji_picker_category_activity">"Activități"</string>
<string name="emoji_picker_category_flags">"Steaguri"</string>
<string name="emoji_picker_category_foods">"Mâncare &amp; Băutură"</string>
<string name="emoji_picker_category_nature">"Animale și Natură"</string>
<string name="emoji_picker_category_objects">"Obiecte"</string>
<string name="emoji_picker_category_people">"Fețe zâmbitoare &amp; Oameni"</string>
<string name="emoji_picker_category_places">"Călătorii &amp; Locuri"</string>
<string name="emoji_picker_category_recent">"Emoticoane recente"</string>
<string name="emoji_picker_category_symbols">"Simboluri"</string>
<string name="screen_media_upload_preview_caption_warning">"Este posibil ca descrierile să nu fie vizibile pentru persoanele care folosesc aplicații mai vechi."</string> <string name="screen_media_upload_preview_caption_warning">"Este posibil ca descrierile să nu fie vizibile pentru persoanele care folosesc aplicații mai vechi."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Atingeți pentru a modifica calitatea încărcării videoclipului"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Atingeți pentru a modifica calitatea încărcării videoclipului"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Fișierul nu a putut fi încărcat."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Fișierul nu a putut fi încărcat."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Зашифровано неизвестным или удаленным устройством."</string> <string name="crypto_event_authenticity_unknown_device">"Зашифровано неизвестным или удаленным устройством."</string>
<string name="crypto_event_authenticity_unsigned_device">"Зашифровано устройством, не проверенным его владельцем."</string> <string name="crypto_event_authenticity_unsigned_device">"Зашифровано устройством, не проверенным его владельцем."</string>
<string name="crypto_event_authenticity_unverified_identity">"Зашифровано непроверенным пользователем."</string> <string name="crypto_event_authenticity_unverified_identity">"Зашифровано непроверенным пользователем."</string>
<string name="emoji_picker_category_activity">"Активности"</string>
<string name="emoji_picker_category_flags">"Флаги"</string>
<string name="emoji_picker_category_foods">"Еда и напитки"</string>
<string name="emoji_picker_category_nature">"Животные и природа"</string>
<string name="emoji_picker_category_objects">"Объекты"</string>
<string name="emoji_picker_category_people">"Эмодзи и люди"</string>
<string name="emoji_picker_category_places">"Путешествия и места"</string>
<string name="emoji_picker_category_recent">"Недавние эмодзи"</string>
<string name="emoji_picker_category_symbols">"Символы"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Перевернуть изображение по горизонтали"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Перевернуть изображение по горизонтали"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Перевернуто по горизонтали"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Перевернуто по горизонтали"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Оригинал"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Оригинал"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Zašifrované neznámym alebo odstráneným zariadením."</string> <string name="crypto_event_authenticity_unknown_device">"Zašifrované neznámym alebo odstráneným zariadením."</string>
<string name="crypto_event_authenticity_unsigned_device">"Šifrované zariadením, ktoré nie je overené jeho majiteľom."</string> <string name="crypto_event_authenticity_unsigned_device">"Šifrované zariadením, ktoré nie je overené jeho majiteľom."</string>
<string name="crypto_event_authenticity_unverified_identity">"Šifrované neovereným používateľom."</string> <string name="crypto_event_authenticity_unverified_identity">"Šifrované neovereným používateľom."</string>
<string name="emoji_picker_category_activity">"Aktivity"</string>
<string name="emoji_picker_category_flags">"Vlajky"</string>
<string name="emoji_picker_category_foods">"Jedlo a nápoje"</string>
<string name="emoji_picker_category_nature">"Zvieratá a príroda"</string>
<string name="emoji_picker_category_objects">"Predmety"</string>
<string name="emoji_picker_category_people">"Smajlíky a ľudia"</string>
<string name="emoji_picker_category_places">"Cestovanie a miesta"</string>
<string name="emoji_picker_category_recent">"Nedávne emotikony"</string>
<string name="emoji_picker_category_symbols">"Symboly"</string>
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie."</string> <string name="screen_media_upload_preview_caption_warning">"Titulky nemusia byť viditeľné pre ľudí používajúcich staršie aplikácie."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Ťuknutím zmeníte kvalitu nahratého videa"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Ťuknutím zmeníte kvalitu nahratého videa"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Súbor sa nepodarilo nahrať."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Súbor sa nepodarilo nahrať."</string>
@@ -7,14 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Krypterad av en okänd eller raderad enhet."</string> <string name="crypto_event_authenticity_unknown_device">"Krypterad av en okänd eller raderad enhet."</string>
<string name="crypto_event_authenticity_unsigned_device">"Krypterad av en enhet som inte verifierats av ägaren."</string> <string name="crypto_event_authenticity_unsigned_device">"Krypterad av en enhet som inte verifierats av ägaren."</string>
<string name="crypto_event_authenticity_unverified_identity">"Krypterad av en overifierad användare."</string> <string name="crypto_event_authenticity_unverified_identity">"Krypterad av en overifierad användare."</string>
<string name="emoji_picker_category_activity">"Aktiviteter"</string>
<string name="emoji_picker_category_flags">"Flaggor"</string>
<string name="emoji_picker_category_foods">"Mat &amp; dryck"</string>
<string name="emoji_picker_category_nature">"Djur &amp; natur"</string>
<string name="emoji_picker_category_objects">"Föremål"</string>
<string name="emoji_picker_category_people">"Smileys &amp; personer"</string>
<string name="emoji_picker_category_places">"Resor &amp; platser"</string>
<string name="emoji_picker_category_symbols">"Symboler"</string>
<string name="screen_media_upload_preview_caption_warning">"Bildtexter kanske inte är synliga för personer som använder äldre appar."</string> <string name="screen_media_upload_preview_caption_warning">"Bildtexter kanske inte är synliga för personer som använder äldre appar."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tryck för att ändra videouppladdningskvaliteten"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Tryck för att ändra videouppladdningskvaliteten"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Filen kunde inte laddas upp."</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"Filen kunde inte laddas upp."</string>
@@ -7,14 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Bilinmeyen veya silinmiş bir cihaz tarafından şifrelenmiştir."</string> <string name="crypto_event_authenticity_unknown_device">"Bilinmeyen veya silinmiş bir cihaz tarafından şifrelenmiştir."</string>
<string name="crypto_event_authenticity_unsigned_device">"Sahibi tarafından doğrulanmamış bir cihaz tarafından şifrelenmiştir."</string> <string name="crypto_event_authenticity_unsigned_device">"Sahibi tarafından doğrulanmamış bir cihaz tarafından şifrelenmiştir."</string>
<string name="crypto_event_authenticity_unverified_identity">"Doğrulanmamış bir kullanıcı tarafından şifrelenmiştir."</string> <string name="crypto_event_authenticity_unverified_identity">"Doğrulanmamış bir kullanıcı tarafından şifrelenmiştir."</string>
<string name="emoji_picker_category_activity">"Aktiviteler"</string>
<string name="emoji_picker_category_flags">"Bayraklar"</string>
<string name="emoji_picker_category_foods">"Yiyecek ve İçecek"</string>
<string name="emoji_picker_category_nature">"Hayvanlar ve Doğa"</string>
<string name="emoji_picker_category_objects">"Nesneler"</string>
<string name="emoji_picker_category_people">"İfadeler ve İnsanlar"</string>
<string name="emoji_picker_category_places">"Seyahat ve Yerler"</string>
<string name="emoji_picker_category_symbols">"Simgeler"</string>
<string name="screen_media_upload_preview_caption_warning">"Açıklamalar, eski uygulamaları kullanan kişiler tarafından görülemeyebilir."</string> <string name="screen_media_upload_preview_caption_warning">"Açıklamalar, eski uygulamaları kullanan kişiler tarafından görülemeyebilir."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Medya yüklenemedi, lütfen tekrar deneyin."</string> <string name="screen_media_upload_preview_error_failed_processing">"Medya yüklenemedi, lütfen tekrar deneyin."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Medya yüklenemedi, lütfen tekrar deneyin."</string> <string name="screen_media_upload_preview_error_failed_sending">"Medya yüklenemedi, lütfen tekrar deneyin."</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Зашифровано невідомим або видаленим пристроєм."</string> <string name="crypto_event_authenticity_unknown_device">"Зашифровано невідомим або видаленим пристроєм."</string>
<string name="crypto_event_authenticity_unsigned_device">"Зашифровано пристроєм, який не звірено його власником."</string> <string name="crypto_event_authenticity_unsigned_device">"Зашифровано пристроєм, який не звірено його власником."</string>
<string name="crypto_event_authenticity_unverified_identity">"Зашифровано не звіреним користувачем."</string> <string name="crypto_event_authenticity_unverified_identity">"Зашифровано не звіреним користувачем."</string>
<string name="emoji_picker_category_activity">"Дії"</string>
<string name="emoji_picker_category_flags">"Прапори"</string>
<string name="emoji_picker_category_foods">"Їжа та напої"</string>
<string name="emoji_picker_category_nature">"Тварини та природа"</string>
<string name="emoji_picker_category_objects">"Об\'єкти"</string>
<string name="emoji_picker_category_people">"Смайлики та люди"</string>
<string name="emoji_picker_category_places">"Подорожі та місця"</string>
<string name="emoji_picker_category_recent">"Нещодавні емодзі"</string>
<string name="emoji_picker_category_symbols">"Символи"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Повернути зображення по горизонталі"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Повернути зображення по горизонталі"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Перевернуто по горизонталі"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Перевернуто по горизонталі"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Оригінал"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Оригінал"</string>
@@ -6,14 +6,6 @@
<string name="crypto_event_authenticity_unknown_device">"کسی نامعلوم یا حذف شدہ آلے کے ذریعے مرموز کردہ۔"</string> <string name="crypto_event_authenticity_unknown_device">"کسی نامعلوم یا حذف شدہ آلے کے ذریعے مرموز کردہ۔"</string>
<string name="crypto_event_authenticity_unsigned_device">"کسی ایسے آلے کے ذریعے مرموز کردہ جس کی توثیق اسکے مالک سے نہیں ہوئی۔"</string> <string name="crypto_event_authenticity_unsigned_device">"کسی ایسے آلے کے ذریعے مرموز کردہ جس کی توثیق اسکے مالک سے نہیں ہوئی۔"</string>
<string name="crypto_event_authenticity_unverified_identity">"ایک غیر توثیق شدہ صارف کے ذریعے مرموز کردہ۔"</string> <string name="crypto_event_authenticity_unverified_identity">"ایک غیر توثیق شدہ صارف کے ذریعے مرموز کردہ۔"</string>
<string name="emoji_picker_category_activity">"سرگرمیاں"</string>
<string name="emoji_picker_category_flags">"جھنڈے"</string>
<string name="emoji_picker_category_foods">"طعم و مشروب"</string>
<string name="emoji_picker_category_nature">"حیوانات و فطرت"</string>
<string name="emoji_picker_category_objects">"اشیاء"</string>
<string name="emoji_picker_category_people">"مسکراہٹیں و لوگ"</string>
<string name="emoji_picker_category_places">"سفر و مقامات"</string>
<string name="emoji_picker_category_symbols">"علامتیں"</string>
<string name="screen_media_upload_preview_error_failed_processing">"وسائط کا معالجہ برائے ترفیع ناکام، برائے مہربانی دوبارہ کوشش کریں۔"</string> <string name="screen_media_upload_preview_error_failed_processing">"وسائط کا معالجہ برائے ترفیع ناکام، برائے مہربانی دوبارہ کوشش کریں۔"</string>
<string name="screen_media_upload_preview_error_failed_sending">"وسائط رفع کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔"</string> <string name="screen_media_upload_preview_error_failed_sending">"وسائط رفع کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔"</string>
<string name="screen_report_content_block_user">"صارف کو مسدود کریں"</string> <string name="screen_report_content_block_user">"صارف کو مسدود کریں"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Nomaʼlum yoki oʻchirib tashlangan qurilma tomonidan shifrlangan."</string> <string name="crypto_event_authenticity_unknown_device">"Nomaʼlum yoki oʻchirib tashlangan qurilma tomonidan shifrlangan."</string>
<string name="crypto_event_authenticity_unsigned_device">"Egasi tasdiqlamagan qurilma tomonidan shifrlangan."</string> <string name="crypto_event_authenticity_unsigned_device">"Egasi tasdiqlamagan qurilma tomonidan shifrlangan."</string>
<string name="crypto_event_authenticity_unverified_identity">"Tasdiqlanmagan foydalanuvchi tomonidan shifrlangan."</string> <string name="crypto_event_authenticity_unverified_identity">"Tasdiqlanmagan foydalanuvchi tomonidan shifrlangan."</string>
<string name="emoji_picker_category_activity">"Faoliyatlar"</string>
<string name="emoji_picker_category_flags">"Bayroqlar"</string>
<string name="emoji_picker_category_foods">"Oziq-ovqat va ichimliklar"</string>
<string name="emoji_picker_category_nature">"Hayvonlar va tabiat"</string>
<string name="emoji_picker_category_objects">"Ob\'ektlar"</string>
<string name="emoji_picker_category_people">"Smayllar va odamlar"</string>
<string name="emoji_picker_category_places">"Sayohat va Joylar"</string>
<string name="emoji_picker_category_recent">"Oxirgi kulgichlar"</string>
<string name="emoji_picker_category_symbols">"Belgilar"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Rasmni gorizontal ravishda aylantiring"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Rasmni gorizontal ravishda aylantiring"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Gorizontal ravishda aylantirildi"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Gorizontal ravishda aylantirildi"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Asl nusxasi"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Asl nusxasi"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Được mã hóa bởi một thiết bị không xác định hoặc đã bị xóa."</string> <string name="crypto_event_authenticity_unknown_device">"Được mã hóa bởi một thiết bị không xác định hoặc đã bị xóa."</string>
<string name="crypto_event_authenticity_unsigned_device">"Được mã hóa bởi một thiết bị chưa được chủ sở hữu xác minh."</string> <string name="crypto_event_authenticity_unsigned_device">"Được mã hóa bởi một thiết bị chưa được chủ sở hữu xác minh."</string>
<string name="crypto_event_authenticity_unverified_identity">"Được mã hóa bởi một người dùng chưa được xác minh."</string> <string name="crypto_event_authenticity_unverified_identity">"Được mã hóa bởi một người dùng chưa được xác minh."</string>
<string name="emoji_picker_category_activity">"Hoạt động"</string>
<string name="emoji_picker_category_flags">"Cờ"</string>
<string name="emoji_picker_category_foods">"Thực phẩm và đồ uống"</string>
<string name="emoji_picker_category_nature">"Động vật và thiên nhiên"</string>
<string name="emoji_picker_category_objects">"Đồ vật"</string>
<string name="emoji_picker_category_people">"Mặt cười &amp; mọi người"</string>
<string name="emoji_picker_category_places">"Du lịch và địa danh"</string>
<string name="emoji_picker_category_recent">"Biểu tượng cảm xúc gần đây"</string>
<string name="emoji_picker_category_symbols">"Biểu tượng"</string>
<string name="screen_media_upload_preview_error_failed_processing">"Xử lý phương tiện tải lên không thành công, vui lòng thử lại."</string> <string name="screen_media_upload_preview_error_failed_processing">"Xử lý phương tiện tải lên không thành công, vui lòng thử lại."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Không thể tải lên tệp phương tiện. Vui lòng thử lại."</string> <string name="screen_media_upload_preview_error_failed_sending">"Không thể tải lên tệp phương tiện. Vui lòng thử lại."</string>
<string name="screen_report_content_block_user">"Chặn người dùng"</string> <string name="screen_report_content_block_user">"Chặn người dùng"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"由未知或已刪除的裝置加密。"</string> <string name="crypto_event_authenticity_unknown_device">"由未知或已刪除的裝置加密。"</string>
<string name="crypto_event_authenticity_unsigned_device">"由未經其擁有者驗證的裝置加密。"</string> <string name="crypto_event_authenticity_unsigned_device">"由未經其擁有者驗證的裝置加密。"</string>
<string name="crypto_event_authenticity_unverified_identity">"由未經驗證的使用者加密。"</string> <string name="crypto_event_authenticity_unverified_identity">"由未經驗證的使用者加密。"</string>
<string name="emoji_picker_category_activity">"活動"</string>
<string name="emoji_picker_category_flags">"旗幟"</string>
<string name="emoji_picker_category_foods">"食物與飲料"</string>
<string name="emoji_picker_category_nature">"動物與大自然"</string>
<string name="emoji_picker_category_objects">"物品"</string>
<string name="emoji_picker_category_people">"表情與人物"</string>
<string name="emoji_picker_category_places">"旅行與景點"</string>
<string name="emoji_picker_category_recent">"最近使用的表情符號"</string>
<string name="emoji_picker_category_symbols">"標誌"</string>
<string name="screen_media_upload_preview_caption_warning">"使用舊應用程式的使用者可能看不到標題。"</string> <string name="screen_media_upload_preview_caption_warning">"使用舊應用程式的使用者可能看不到標題。"</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"輕點即可變更影片上傳品質"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"輕點即可變更影片上傳品質"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"無法上傳檔案。"</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"無法上傳檔案。"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"由未知或已删除的设备加密。"</string> <string name="crypto_event_authenticity_unknown_device">"由未知或已删除的设备加密。"</string>
<string name="crypto_event_authenticity_unsigned_device">"由未经其所有者验证的设备加密。"</string> <string name="crypto_event_authenticity_unsigned_device">"由未经其所有者验证的设备加密。"</string>
<string name="crypto_event_authenticity_unverified_identity">"由未经验证的用户加密。"</string> <string name="crypto_event_authenticity_unverified_identity">"由未经验证的用户加密。"</string>
<string name="emoji_picker_category_activity">"节假日"</string>
<string name="emoji_picker_category_flags">"旗帜"</string>
<string name="emoji_picker_category_foods">"饮食"</string>
<string name="emoji_picker_category_nature">"动物和自然"</string>
<string name="emoji_picker_category_objects">"日常物品"</string>
<string name="emoji_picker_category_people">"表情和人物"</string>
<string name="emoji_picker_category_places">"文旅景点"</string>
<string name="emoji_picker_category_recent">"最近的 Emoji"</string>
<string name="emoji_picker_category_symbols">"符号"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"水平翻转图像"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"水平翻转图像"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"水平翻转"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"水平翻转"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"原始"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"原始"</string>
@@ -7,15 +7,6 @@
<string name="crypto_event_authenticity_unknown_device">"Encrypted by an unknown or deleted device."</string> <string name="crypto_event_authenticity_unknown_device">"Encrypted by an unknown or deleted device."</string>
<string name="crypto_event_authenticity_unsigned_device">"Encrypted by a device not verified by its owner."</string> <string name="crypto_event_authenticity_unsigned_device">"Encrypted by a device not verified by its owner."</string>
<string name="crypto_event_authenticity_unverified_identity">"Encrypted by an unverified user."</string> <string name="crypto_event_authenticity_unverified_identity">"Encrypted by an unverified user."</string>
<string name="emoji_picker_category_activity">"Activities"</string>
<string name="emoji_picker_category_flags">"Flags"</string>
<string name="emoji_picker_category_foods">"Food &amp; Drink"</string>
<string name="emoji_picker_category_nature">"Animals &amp; Nature"</string>
<string name="emoji_picker_category_objects">"Objects"</string>
<string name="emoji_picker_category_people">"Smileys &amp; People"</string>
<string name="emoji_picker_category_places">"Travel &amp; Places"</string>
<string name="emoji_picker_category_recent">"Recent emojis"</string>
<string name="emoji_picker_category_symbols">"Symbols"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally">"Flip image horizontally"</string> <string name="screen_image_edition_a11y_flip_image_horizontally">"Flip image horizontally"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Flipped horizontally"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Flipped horizontally"</string>
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Original"</string> <string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Original"</string>
@@ -52,6 +52,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.emoji.api.recentemojis.AddRecentEmoji
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@@ -94,7 +95,6 @@ import io.element.android.libraries.matrix.test.room.threads.FakeThreadsListServ
import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.aTimelineItemDebugInfo import io.element.android.libraries.matrix.test.timeline.aTimelineItemDebugInfo
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.recentemojis.api.AddRecentEmoji
import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.TextEditorState
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
@@ -31,7 +31,6 @@ import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.Emoji
import io.element.android.emojibasebindings.EmojibaseCategory import io.element.android.emojibasebindings.EmojibaseCategory
import io.element.android.emojibasebindings.EmojibaseStore
import io.element.android.features.messages.impl.actionlist.ActionListEvent import io.element.android.features.messages.impl.actionlist.ActionListEvent
import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.anActionListState
@@ -48,6 +47,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemList
import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts
import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
import io.element.android.features.messages.impl.timeline.aTimelineState import io.element.android.features.messages.impl.timeline.aTimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent
@@ -56,6 +56,8 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.features.roomcall.api.aStandByCallState
import io.element.android.libraries.emoji.impl.picker.DefaultEmojiPickerRenderer
import io.element.android.libraries.emoji.impl.picker.anEmojiPickerState
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
@@ -538,12 +540,7 @@ class MessagesViewTest : RobolectricTest() {
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
val stateWithCustomReactionState = state.copy( val emojisByCategory = persistentMapOf(
customReactionState = aCustomReactionState(
target = CustomReactionState.Target.Success(
event = timelineItem,
emojibaseStore = EmojibaseStore(
categories = persistentMapOf(
EmojibaseCategory.People to persistentListOf( EmojibaseCategory.People to persistentListOf(
Emoji( Emoji(
hexcode = "", hexcode = "",
@@ -555,7 +552,11 @@ class MessagesViewTest : RobolectricTest() {
) )
) )
) )
), val stateWithCustomReactionState = state.copy(
customReactionState = aCustomReactionState(
target = CustomReactionState.Target.Success(
event = timelineItem,
emojiPickerState = anEmojiPickerState(emojis = emojisByCategory),
), ),
eventSink = customReactionStateEventsRecorder eventSink = customReactionStateEventsRecorder
), ),
@@ -695,6 +696,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setMessagesView(
onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
onThreadsListClicked: () -> Unit = EnsureNeverCalled(), onThreadsListClicked: () -> Unit = EnsureNeverCalled(),
) { ) {
val emojiPickerRenderer = DefaultEmojiPickerRenderer()
setSafeContent { setSafeContent {
// Cannot use the RichTextEditor, so simulate a LocalInspectionMode // Cannot use the RichTextEditor, so simulate a LocalInspectionMode
CompositionLocalProvider(LocalInspectionMode provides true) { CompositionLocalProvider(LocalInspectionMode provides true) {
@@ -711,6 +713,15 @@ private fun AndroidComposeUiTest<ComponentActivity>.setMessagesView(
onJoinCallClick = onJoinCallClick, onJoinCallClick = onJoinCallClick,
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
knockRequestsBannerView = {}, knockRequestsBannerView = {},
customReactionBottomSheet = {
CustomReactionBottomSheet(
state = state.customReactionState,
onSelectEmoji = { uniqueId, emoji ->
state.eventSink(MessagesEvent.ToggleReaction(emoji.unicode, uniqueId))
},
emojiPickerRenderer = emojiPickerRenderer,
)
},
onThreadsListClick = onThreadsListClicked, onThreadsListClick = onThreadsListClicked,
) )
} }
@@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.notification.CallIntent
@@ -42,7 +43,6 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.recentemojis.api.GetRecentEmojis
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -11,11 +11,11 @@ package io.element.android.features.messages.impl.timeline.components.customreac
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
import io.element.android.libraries.emoji.api.recentemojis.EmptyGetRecentEmojis
import io.element.android.libraries.emoji.test.fakeEmojiPickerPresenterFactory
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.recentemojis.test.FakeEmojibaseProvider
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -25,8 +25,8 @@ class CustomReactionPresenterTest {
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val presenter = CustomReactionPresenter( private val presenter = CustomReactionPresenter(
emojibaseProvider = FakeEmojibaseProvider(), emojiPickerPresenterFactory = fakeEmojiPickerPresenterFactory(),
getRecentEmojis = { Result.success(persistentListOf()) }, getRecentEmojis = EmptyGetRecentEmojis,
) )
@Test @Test
@@ -38,8 +38,6 @@ class CustomReactionPresenterTest {
initialState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event)) initialState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event))
assertThat(awaitItem().target).isEqualTo(CustomReactionState.Target.Loading(event))
val eventId = (awaitItem().target as? CustomReactionState.Target.Success)?.event?.eventId val eventId = (awaitItem().target as? CustomReactionState.Target.Success)?.event?.eventId
assertThat(eventId).isEqualTo(AN_EVENT_ID) assertThat(eventId).isEqualTo(AN_EVENT_ID)
@@ -59,8 +57,6 @@ class CustomReactionPresenterTest {
val key = reactions.reactions.first().key val key = reactions.reactions.first().key
initialState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event)) initialState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event))
assertThat(awaitItem().target).isEqualTo(CustomReactionState.Target.Loading(event))
val stateWithSelectedEmojis = awaitItem() val stateWithSelectedEmojis = awaitItem()
val eventId = (stateWithSelectedEmojis.target as? CustomReactionState.Target.Success)?.event?.eventId val eventId = (stateWithSelectedEmojis.target as? CustomReactionState.Target.Success)?.event?.eventId
assertThat(eventId).isEqualTo(AN_EVENT_ID) assertThat(eventId).isEqualTo(AN_EVENT_ID)
@@ -1,125 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector 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.features.messages.impl.timeline.components.customreaction.picker
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.currentComposer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.test.platform.app.InstrumentationRegistry
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.emojibasebindings.Emoji
import io.element.android.emojibasebindings.EmojibaseCategory
import io.element.android.emojibasebindings.EmojibaseStore
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.robolectric.RobolectricTest
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class EmojiPickerPresenterTest : RobolectricTest() {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `updating search query loads new results`() = runTest {
testPresenter {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.searchQuery.text.toString()).isEmpty()
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
initialState.searchQuery.setTextAndPlaceCursorAtEnd("smile")
assertThat(awaitItem().searchQuery.text.toString()).isEqualTo("smile")
val stateWithResults = awaitItem()
assertThat(stateWithResults.searchQuery.text.toString()).isEqualTo("smile")
assertThat(stateWithResults.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
}
}
@Test
fun `ToggleSearchActive toggles the search state`() = runTest {
testPresenter {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.isSearchActive).isFalse()
initialState.eventSink(EmojiPickerEvent.ToggleSearchActive(true))
assertThat(awaitItem().isSearchActive).isTrue()
initialState.eventSink(EmojiPickerEvent.ToggleSearchActive(false))
assertThat(awaitItem().isSearchActive).isFalse()
}
}
@Test
fun `recent emojis are automatically added to the categories if present`() = runTest {
val providedCategories = persistentListOf(emojiCategory(EmojibaseCategory.Activity))
val presenter = createPresenter(
categories = providedCategories,
recentEmojis = persistentListOf("😊"),
)
testPresenter(presenter) {
skipItems(1)
val initialState = awaitItem()
assertThat(providedCategories.size).isNotEqualTo(initialState.categories.size)
assertThat(initialState.categories.size).isEqualTo(2)
}
}
private fun TestScope.createPresenter(
categories: ImmutableList<Pair<EmojibaseCategory, ImmutableList<Emoji>>> = persistentListOf(emojiCategory()),
recentEmojis: ImmutableList<String> = persistentListOf(),
) = EmojiPickerPresenter(
emojibaseStore = EmojibaseStore(categories.toMap().toImmutableMap()),
recentEmojis = recentEmojis,
coroutineDispatchers = testCoroutineDispatchers(),
)
private fun emojiCategory(
category: EmojibaseCategory = EmojibaseCategory.Activity,
emojis: ImmutableList<Emoji> = persistentListOf(
Emoji("1F3C3", "Smile", persistentListOf("smile"), persistentListOf("smile"), "😊", skins = null)
)
) = category to emojis
@OptIn(InternalComposeApi::class)
private suspend fun TestScope.testPresenter(
presenter: EmojiPickerPresenter = createPresenter(),
testBlock: suspend TurbineTestContext<EmojiPickerState>.() -> Unit,
) {
moleculeFlow(RecompositionMode.Immediate) {
// These are needed to load the history icon in the presenter
currentComposer.startProviders(arrayOf(
LocalContext provides InstrumentationRegistry.getInstrumentation().context,
LocalConfiguration provides InstrumentationRegistry.getInstrumentation().context.resources.configuration,
))
val state = presenter.present()
currentComposer.endProviders()
state
}.test {
testBlock()
}
}
}
@@ -81,6 +81,7 @@ dependencies {
implementation(projects.features.deactivation.api) implementation(projects.features.deactivation.api)
implementation(projects.features.home.api) implementation(projects.features.home.api)
implementation(projects.features.invite.api) implementation(projects.features.invite.api)
implementation(projects.libraries.emoji.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(projects.services.analytics.compose) implementation(projects.services.analytics.compose)
implementation(projects.services.appnavstate.api) implementation(projects.services.appnavstate.api)
@@ -98,6 +99,7 @@ dependencies {
testCommonDependencies(libs, true) testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.emoji.test)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediaupload.test)
@@ -24,6 +24,7 @@ import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@@ -33,6 +34,7 @@ class PreferencesRootNode(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: PreferencesRootPresenter, private val presenter: PreferencesRootPresenter,
private val directLogoutView: DirectLogoutView, private val directLogoutView: DirectLogoutView,
private val emojiPickerRenderer: EmojiPickerRenderer,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun navigateToAddAccount() fun navigateToAddAccount()
@@ -75,6 +77,7 @@ class PreferencesRootNode(
val isDark = ElementTheme.isLightTheme.not() val isDark = ElementTheme.isLightTheme.not()
PreferencesRootView( PreferencesRootView(
state = state, state = state,
emojiPickerRenderer = emojiPickerRenderer,
modifier = modifier, modifier = modifier,
onBackClick = this::navigateUp, onBackClick = this::navigateUp,
onAddAccountClick = callback::navigateToAddAccount, onAddAccountClick = callback::navigateToAddAccount,
@@ -41,6 +41,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.matrix.ui.components.MatrixUserRow
@@ -49,6 +51,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun PreferencesRootView( fun PreferencesRootView(
state: PreferencesRootState, state: PreferencesRootState,
emojiPickerRenderer: EmojiPickerRenderer,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onAddAccountClick: () -> Unit, onAddAccountClick: () -> Unit,
onSecureBackupClick: () -> Unit, onSecureBackupClick: () -> Unit,
@@ -90,7 +93,11 @@ fun PreferencesRootView(
) )
} }
if (state.userStatusState != null) { if (state.userStatusState != null) {
UserStatusSection(state.userStatusState, showTopDivider = !state.isMultiAccountEnabled) UserStatusSection(
userStatusState = state.userStatusState,
emojiPickerRenderer = emojiPickerRenderer,
showTopDivider = !state.isMultiAccountEnabled,
)
} }
// 'Account' section // 'Account' section
ManageAccountSection( ManageAccountSection(
@@ -135,6 +142,7 @@ fun PreferencesRootView(
@Composable @Composable
private fun ColumnScope.UserStatusSection( private fun ColumnScope.UserStatusSection(
userStatusState: UserStatusState, userStatusState: UserStatusState,
emojiPickerRenderer: EmojiPickerRenderer,
showTopDivider: Boolean, showTopDivider: Boolean,
) { ) {
if (showTopDivider) { if (showTopDivider) {
@@ -145,6 +153,7 @@ private fun ColumnScope.UserStatusSection(
} }
UserStatusView( UserStatusView(
state = userStatusState, state = userStatusState,
emojiPickerRenderer = emojiPickerRenderer,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
HorizontalDivider( HorizontalDivider(
@@ -369,6 +378,7 @@ internal fun PreferencesRootViewDarkPreview(@PreviewParameter(PreferencesRootSta
private fun ContentToPreview(state: PreferencesRootState) { private fun ContentToPreview(state: PreferencesRootState) {
PreferencesRootView( PreferencesRootView(
state = state, state = state,
emojiPickerRenderer = NoOpEmojiPickerRenderer,
onBackClick = {}, onBackClick = {},
onAddAccountClick = {}, onAddAccountClick = {},
onOpenAnalytics = {}, onOpenAnalytics = {},
@@ -16,5 +16,7 @@ sealed interface UserStatusEvent {
data object OpenCustomInput : UserStatusEvent data object OpenCustomInput : UserStatusEvent
data object CancelCustomInput : UserStatusEvent data object CancelCustomInput : UserStatusEvent
data class UpdateCustomEmoji(val emoji: String) : UserStatusEvent data class UpdateCustomEmoji(val emoji: String) : UserStatusEvent
data object OpenEmojiPicker : UserStatusEvent
data object DismissEmojiPicker : UserStatusEvent
data object ClearStatus : UserStatusEvent data object ClearStatus : UserStatusEvent
} }
@@ -19,36 +19,32 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.emoji.api.picker.EmojiPickerPresenter
import io.element.android.libraries.emoji.api.recentemojis.EmptyGetRecentEmojis
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Inject @Inject
class UserStatusPresenter( class UserStatusPresenter(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
emojiPickerPresenterFactory: EmojiPickerPresenter.Factory,
) : Presenter<UserStatusState> { ) : Presenter<UserStatusState> {
private val emojiPickerPresenter = emojiPickerPresenterFactory.create(EmptyGetRecentEmojis)
@Composable @Composable
override fun present(): UserStatusState { override fun present(): UserStatusState {
val userProfile by matrixClient.userProfile.collectAsState() val userProfile by matrixClient.userProfile.collectAsState()
var pickerState by remember { mutableStateOf<UserStatusPickerState>(UserStatusPickerState.Hidden) } var pickerState by remember { mutableStateOf<UserStatusPickerState>(UserStatusPickerState.Hidden) }
var isEmojiPickerVisible by remember { mutableStateOf(false) }
val customTextFieldState = rememberTextFieldState() val customTextFieldState = rememberTextFieldState()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
fun handleEvent(event: UserStatusEvent) { fun handleEvent(event: UserStatusEvent) {
when (event) { when (event) {
// Mode transitions
UserStatusEvent.OpenPicker -> pickerState = UserStatusPickerState.ShowingPicker UserStatusEvent.OpenPicker -> pickerState = UserStatusPickerState.ShowingPicker
UserStatusEvent.DismissPicker -> pickerState = UserStatusPickerState.Hidden UserStatusEvent.DismissPicker -> pickerState = UserStatusPickerState.Hidden
UserStatusEvent.OpenCustomInput -> { UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden
val raw = userProfile.rawStatus
if (raw != null) {
customTextFieldState.setTextAndPlaceCursorAtEnd(raw.text)
} else {
customTextFieldState.clearText()
}
pickerState = UserStatusPickerState.CustomInput(
emoji = raw?.emoji ?: "😀",
textFieldState = customTextFieldState,
)
}
is UserStatusEvent.SetStatus -> { is UserStatusEvent.SetStatus -> {
pickerState = UserStatusPickerState.Hidden pickerState = UserStatusPickerState.Hidden
coroutineScope.launch { matrixClient.setUserStatus(event.status) } coroutineScope.launch { matrixClient.setUserStatus(event.status) }
@@ -57,19 +53,58 @@ class UserStatusPresenter(
pickerState = UserStatusPickerState.Hidden pickerState = UserStatusPickerState.Hidden
coroutineScope.launch { matrixClient.clearUserStatus() } coroutineScope.launch { matrixClient.clearUserStatus() }
} }
UserStatusEvent.CancelCustomInput -> pickerState = UserStatusPickerState.Hidden UserStatusEvent.OpenCustomInput -> {
val raw = userProfile.rawStatus
if (raw != null) {
customTextFieldState.setTextAndPlaceCursorAtEnd(raw.text)
} else {
customTextFieldState.clearText()
}
isEmojiPickerVisible = false
pickerState = UserStatusPickerState.CustomInput(
emoji = raw?.emoji ?: "😀",
textFieldState = customTextFieldState,
emojiPickerSheetState = EmojiPickerSheetState.Hidden,
)
}
// Custom-input mutations
is UserStatusEvent.UpdateCustomEmoji -> { is UserStatusEvent.UpdateCustomEmoji -> {
isEmojiPickerVisible = false
val current = pickerState as? UserStatusPickerState.CustomInput ?: return val current = pickerState as? UserStatusPickerState.CustomInput ?: return
pickerState = current.copy(emoji = event.emoji) pickerState = current.copy(emoji = event.emoji)
} }
// Emoji-picker visibility (only meaningful within CustomInput mode)
UserStatusEvent.OpenEmojiPicker -> isEmojiPickerVisible = true
UserStatusEvent.DismissEmojiPicker -> isEmojiPickerVisible = false
} }
} }
// In CustomInput mode, override the picker state's `emojiPickerSheetState` with the freshly composed emoji-picker
// sub-state so recompositions of the picker propagate up. Other modes pass through unchanged.
val effectivePickerState = when (val current = pickerState) {
is UserStatusPickerState.CustomInput -> {
val emojiPickerState = presentEmojiPickerSheetState(isEmojiPickerVisible)
current.copy(emojiPickerSheetState = emojiPickerState)
}
else -> current
}
return UserStatusState( return UserStatusState(
displayedStatus = userProfile.displayedStatus, displayedStatus = userProfile.displayedStatus,
rawStatus = userProfile.rawStatus, rawStatus = userProfile.rawStatus,
pickerState = pickerState, pickerState = effectivePickerState,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
@Composable
private fun presentEmojiPickerSheetState(isVisible: Boolean): EmojiPickerSheetState {
if (!isVisible) return EmojiPickerSheetState.Hidden
val emojiPickerState = emojiPickerPresenter.present()
return if (emojiPickerState.isReady) {
EmojiPickerSheetState.Shown(emojiPickerState)
} else {
EmojiPickerSheetState.Loading
}
}
} }
@@ -11,6 +11,7 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.emoji.api.picker.EmojiPickerState
import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.UserStatus import io.element.android.libraries.matrix.api.user.UserStatus
@@ -28,13 +29,21 @@ sealed interface UserStatusPickerState {
data class CustomInput( data class CustomInput(
val emoji: String, val emoji: String,
val textFieldState: TextFieldState, val textFieldState: TextFieldState,
val emojiPickerSheetState: EmojiPickerSheetState,
) : UserStatusPickerState ) : UserStatusPickerState
} }
enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) { @Immutable
IN_A_MEETING("💬", R.string.common_user_status_in_a_meeting), sealed interface EmojiPickerSheetState {
FOCUS_TIME("💡", R.string.common_user_status_focus_time), data object Hidden : EmojiPickerSheetState
ON_THE_ROAD("🚙", R.string.common_user_status_on_the_road), data object Loading : EmojiPickerSheetState
BE_RIGHT_BACK("☕", R.string.common_user_status_be_right_back), data class Shown(val state: EmojiPickerState) : EmojiPickerSheetState
AWAY("🌴", R.string.common_user_status_away), }
enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) {
IN_A_MEETING("💬", R.string.screen_settings_user_status_in_a_meeting),
FOCUS_TIME("💡", R.string.screen_settings_user_status_focus_time),
ON_THE_ROAD("🚙", R.string.screen_settings_user_status_on_the_road),
BE_RIGHT_BACK("☕", R.string.screen_settings_user_status_be_right_back),
AWAY("🌴", R.string.screen_settings_user_status_away),
} }
@@ -23,8 +23,20 @@ internal class UserStatusStateProvider : PreviewParameterProvider<UserStatusStat
rawStatus = UserStatus("🌴", "Away"), rawStatus = UserStatus("🌴", "Away"),
pickerState = UserStatusPickerState.ShowingPicker, pickerState = UserStatusPickerState.ShowingPicker,
), ),
aUserStatusState(pickerState = UserStatusPickerState.CustomInput(emoji = "😀", textFieldState = TextFieldState())), aUserStatusState(
aUserStatusState(pickerState = UserStatusPickerState.CustomInput(emoji = "🚀", textFieldState = TextFieldState("Working on something"))), pickerState = UserStatusPickerState.CustomInput(
emoji = "😀",
textFieldState = TextFieldState(),
emojiPickerSheetState = EmojiPickerSheetState.Hidden
)
),
aUserStatusState(
pickerState = UserStatusPickerState.CustomInput(
emoji = "🚀",
textFieldState = TextFieldState("Working on something"),
emojiPickerSheetState = EmojiPickerSheetState.Hidden
)
),
) )
} }
@@ -10,6 +10,7 @@ package io.element.android.features.preferences.impl.userstatus
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -58,13 +59,17 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.hide import io.element.android.libraries.designsystem.theme.components.hide
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.UserStatus import io.element.android.libraries.matrix.api.user.UserStatus
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentSetOf
@Composable @Composable
fun UserStatusView( fun UserStatusView(
state: UserStatusState, state: UserStatusState,
emojiPickerRenderer: EmojiPickerRenderer,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (val pickerState = state.pickerState) { when (val pickerState = state.pickerState) {
@@ -103,8 +108,18 @@ fun UserStatusView(
) )
}, },
onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) }, onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) },
onEmojiClick = { state.eventSink(UserStatusEvent.OpenEmojiPicker) },
modifier = modifier, modifier = modifier,
) )
when (val sheet = pickerState.emojiPickerSheetState) {
EmojiPickerSheetState.Hidden, EmojiPickerSheetState.Loading -> Unit
is EmojiPickerSheetState.Shown -> EmojiPickerBottomSheet(
pickerState = sheet.state,
emojiPickerRenderer = emojiPickerRenderer,
onSelectEmoji = { emoji -> state.eventSink(UserStatusEvent.UpdateCustomEmoji(emoji.unicode)) },
onDismiss = { state.eventSink(UserStatusEvent.DismissEmojiPicker) },
)
}
} }
} }
} }
@@ -117,7 +132,7 @@ private fun EmptyStatusRow(
ListItem( ListItem(
headlineContent = { headlineContent = {
Text( Text(
text = stringResource(R.string.screen_preferences_user_status_placeholder), text = stringResource(R.string.screen_settings_user_status_placeholder),
modifier = Modifier.padding(vertical = 16.dp), modifier = Modifier.padding(vertical = 16.dp),
) )
}, },
@@ -135,7 +150,7 @@ private fun CurrentStatusRow(
) { ) {
val (emoji, text) = when (displayedStatus) { val (emoji, text) = when (displayedStatus) {
is DisplayedStatus.UserSet -> displayedStatus.status.emoji to displayedStatus.status.text is DisplayedStatus.UserSet -> displayedStatus.status.emoji to displayedStatus.status.text
is DisplayedStatus.InCall -> "🎧" to stringResource(R.string.common_user_status_on_a_call) is DisplayedStatus.InCall -> "🎧" to stringResource(CommonStrings.common_on_a_call)
} }
ListItem( ListItem(
headlineContent = { Text(text = text, modifier = Modifier.padding(vertical = 16.dp)) }, headlineContent = { Text(text = text, modifier = Modifier.padding(vertical = 16.dp)) },
@@ -210,7 +225,7 @@ private fun UserStatusPickerBottomSheet(
} }
ListItem( ListItem(
headlineContent = { headlineContent = {
Text(text = stringResource(R.string.common_user_status_custom)) Text(text = stringResource(R.string.screen_settings_user_status_custom))
}, },
leadingContent = ListItemContent.Custom { EmojiText("✏️") }, leadingContent = ListItemContent.Custom { EmojiText("✏️") },
onClick = { onClick = {
@@ -231,6 +246,7 @@ private fun CustomStatusInputRow(
rawStatus: UserStatus?, rawStatus: UserStatus?,
onConfirm: () -> Unit, onConfirm: () -> Unit,
onCancel: () -> Unit, onCancel: () -> Unit,
onEmojiClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val hasText by remember { derivedStateOf { textFieldState.text.isNotEmpty() } } val hasText by remember { derivedStateOf { textFieldState.text.isNotEmpty() } }
@@ -249,7 +265,7 @@ private fun CustomStatusInputRow(
TextField( TextField(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
state = textFieldState, state = textFieldState,
placeholder = stringResource(R.string.screen_preferences_user_status_custom_hint), placeholder = stringResource(R.string.screen_settings_user_status_custom_hint),
inputTransformation = InputTransformation.maxLength(CUSTOM_STATUS_TEXT_MAX_LENGTH), inputTransformation = InputTransformation.maxLength(CUSTOM_STATUS_TEXT_MAX_LENGTH),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
onKeyboardAction = { if (hasChanges) onConfirm() }, onKeyboardAction = { if (hasChanges) onConfirm() },
@@ -297,7 +313,7 @@ private fun CustomStatusInputRow(
modifier = Modifier modifier = Modifier
.size(50.dp) .size(50.dp)
.clip(CircleShape) .clip(CircleShape)
.clickable { }, .clickable(onClick = onEmojiClick),
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
EmojiText(emoji) EmojiText(emoji)
@@ -308,8 +324,39 @@ private fun CustomStatusInputRow(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun EmojiPickerBottomSheet(
pickerState: io.element.android.libraries.emoji.api.picker.EmojiPickerState,
emojiPickerRenderer: EmojiPickerRenderer,
onSelectEmoji: (io.element.android.emojibasebindings.Emoji) -> Unit,
onDismiss: () -> Unit,
) {
val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden)
val coroutineScope = rememberCoroutineScope()
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
scrollable = false,
) {
emojiPickerRenderer.Render(
state = pickerState,
onSelectEmoji = { emoji ->
sheetState.hide(coroutineScope) {
onSelectEmoji(emoji)
}
},
selectedEmojis = persistentSetOf(),
modifier = Modifier.fillMaxSize(),
)
}
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun UserStatusViewPreview(@PreviewParameter(UserStatusStateProvider::class) state: UserStatusState) = ElementPreview { internal fun UserStatusViewPreview(@PreviewParameter(UserStatusStateProvider::class) state: UserStatusState) = ElementPreview {
UserStatusView(state = state) UserStatusView(
state = state,
emojiPickerRenderer = NoOpEmojiPickerRenderer,
)
} }
@@ -101,6 +101,14 @@ If you proceed, some of your settings may change."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"system settings"</string> <string name="screen_notification_settings_system_notifications_action_required_content_link">"system settings"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string> <string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
<string name="screen_notification_settings_title">"Notifications"</string> <string name="screen_notification_settings_title">"Notifications"</string>
<string name="screen_settings_user_status_away">"Away"</string>
<string name="screen_settings_user_status_be_right_back">"Be right back"</string>
<string name="screen_settings_user_status_custom">"Custom…"</string>
<string name="screen_settings_user_status_custom_hint">"Custom status…"</string>
<string name="screen_settings_user_status_focus_time">"Focus time"</string>
<string name="screen_settings_user_status_in_a_meeting">"In a meeting"</string>
<string name="screen_settings_user_status_on_the_road">"On the road"</string>
<string name="screen_settings_user_status_placeholder">"What\'s your status?"</string>
<string name="theme_black">"Black"</string> <string name="theme_black">"Black"</string>
<string name="theme_dark">"Dark"</string> <string name="theme_dark">"Dark"</string>
<string name="theme_light">"Light"</string> <string name="theme_light">"Light"</string>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<resources>
<string name="common_user_status_in_a_meeting">In a meeting</string>
<string name="common_user_status_focus_time">Focus time</string>
<string name="common_user_status_on_the_road">On the road</string>
<string name="common_user_status_be_right_back">Be right back</string>
<string name="common_user_status_away">Away</string>
<string name="common_user_status_on_a_call">On a call</string>
<string name="common_user_status_custom">Custom…</string>
<string name="screen_preferences_user_status_placeholder">What\'s your status?</string>
<string name="screen_preferences_user_status_custom_hint">Custom status…</string>
</resources>
@@ -19,6 +19,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
@@ -466,6 +467,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
setContent { setContent {
PreferencesRootView( PreferencesRootView(
state = state, state = state,
emojiPickerRenderer = NoOpEmojiPickerRenderer,
onBackClick = onBackClick, onBackClick = onBackClick,
onAddAccountClick = onAddAccountClick, onAddAccountClick = onAddAccountClick,
onSecureBackupClick = onSecureBackupClick, onSecureBackupClick = onSecureBackupClick,
@@ -8,6 +8,7 @@
package io.element.android.features.preferences.impl.userstatus package io.element.android.features.preferences.impl.userstatus
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.emoji.test.fakeEmojiPickerPresenterFactory
import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.UserStatus import io.element.android.libraries.matrix.api.user.UserStatus
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
@@ -17,7 +18,10 @@ import org.junit.Test
class UserStatusPresenterTest { class UserStatusPresenterTest {
private fun createPresenter(client: FakeMatrixClient = FakeMatrixClient()) = private fun createPresenter(client: FakeMatrixClient = FakeMatrixClient()) =
UserStatusPresenter(matrixClient = client) UserStatusPresenter(
matrixClient = client,
emojiPickerPresenterFactory = fakeEmojiPickerPresenterFactory(),
)
@Test @Test
fun `initial state - no status set`() = runTest { fun `initial state - no status set`() = runTest {
@@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -75,6 +76,7 @@ fun aDmRoomMember(
role: RoomMember.Role = RoomMember.Role.User, role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null, membershipChangeReason: String? = null,
isServiceMember: Boolean = false, isServiceMember: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = RoomMember( ) = RoomMember(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
@@ -86,6 +88,7 @@ fun aDmRoomMember(
role = role, role = role,
membershipChangeReason = membershipChangeReason, membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember, isServiceMember = isServiceMember,
displayedStatus = displayedStatus,
) )
fun aRoomDetailsState( fun aRoomDetailsState(
@@ -178,12 +181,13 @@ fun aDmRoomDetailsState(
roomName: String = "Daniel", roomName: String = "Daniel",
isEncrypted: Boolean = true, isEncrypted: Boolean = true,
dmRoomMemberVerificationState: UserProfileVerificationState = UserProfileVerificationState.UNKNOWN, dmRoomMemberVerificationState: UserProfileVerificationState = UserProfileVerificationState.UNKNOWN,
displayedStatus: DisplayedStatus? = null,
) = aRoomDetailsState( ) = aRoomDetailsState(
roomName = roomName, roomName = roomName,
isPublic = false, isPublic = false,
isEncrypted = isEncrypted, isEncrypted = isEncrypted,
canInvite = true, canInvite = true,
roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)), roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored, displayedStatus = displayedStatus)),
dmOtherMemberDetailsState = aUserProfileState( dmOtherMemberDetailsState = aUserProfileState(
isBlocked = AsyncData.Success(isDmMemberIgnored), isBlocked = AsyncData.Success(isDmMemberIgnored),
verificationState = dmRoomMemberVerificationState, verificationState = dmRoomMemberVerificationState,
@@ -37,6 +37,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
@@ -88,6 +89,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.toText
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@@ -489,11 +491,23 @@ private fun RoomHeaderSection(
} }
.testTag(TestTags.roomDetailAvatar) .testTag(TestTags.roomDetailAvatar)
) )
TitleAndSubtitle( Spacer(modifier = Modifier.height(24.dp))
title = roomName, Text(
subtitle = roomAlias?.value, text = roomName,
onSubtitleClick = onSubtitleClick, style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center,
) )
if (roomAlias != null) {
Spacer(modifier = Modifier.height(12.dp))
Text(
modifier = Modifier.niceClickable { onSubtitleClick(roomAlias.value) },
text = roomAlias.value,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
Spacer(modifier = Modifier.height(12.dp))
} }
} }
@@ -531,37 +545,34 @@ private fun DmHeaderSection(
} }
.testTag(TestTags.roomDetailAvatar) .testTag(TestTags.roomDetailAvatar)
) )
TitleAndSubtitle(
title = roomName,
subtitle = otherMember.userId.value,
onSubtitleClick = onSubtitleClick,
)
}
}
@Composable
private fun TitleAndSubtitle(
title: String,
subtitle: String?,
onSubtitleClick: (String) -> Unit,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Text( Text(
text = title, text = roomName,
style = ElementTheme.typography.fontHeadingLgBold, style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
if (subtitle != null) { val userStatus = otherMember.displayedStatus?.toText()
Spacer(modifier = Modifier.height(6.dp)) if (userStatus != null) {
Spacer(modifier = Modifier.height(12.dp))
Text( Text(
modifier = Modifier.niceClickable { onSubtitleClick(subtitle) }, text = userStatus,
text = subtitle, style = ElementTheme.typography.fontBodyLgMedium,
maxLines = 1,
color = ElementTheme.colors.textSecondary,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
modifier = Modifier.niceClickable { onSubtitleClick(otherMember.userId.value) },
text = otherMember.userId.value,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
} Spacer(modifier = Modifier.height(12.dp))
} }
} }
@@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.RoomMember 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.room.RoomMembershipState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> { internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
@@ -129,6 +130,7 @@ fun aRoomMember(
role: RoomMember.Role = RoomMember.Role.User, role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null, membershipChangeReason: String? = null,
isServiceMember: Boolean = false, isServiceMember: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = RoomMember( ) = RoomMember(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
@@ -140,6 +142,7 @@ fun aRoomMember(
role = role, role = role,
membershipChangeReason = membershipChangeReason, membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember, isServiceMember = isServiceMember,
displayedStatus = displayedStatus,
) )
fun aRoomMemberList() = persistentListOf( fun aRoomMemberList() = persistentListOf(
@@ -128,6 +128,7 @@ class RoomMemberDetailsPresenter(
return userProfileState.copy( return userProfileState.copy(
userName = roomUserName ?: userProfileState.userName, userName = roomUserName ?: userProfileState.userName,
avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl, avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl,
displayedStatus = roomMember?.displayedStatus ?: userProfileState.displayedStatus,
verificationState = verificationState, verificationState = verificationState,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
eventSink = ::handleEvent, eventSink = ::handleEvent,
@@ -16,6 +16,7 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
import io.element.android.libraries.matrix.api.core.UserId 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 import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -82,6 +83,7 @@ fun anAlice() = MatrixUser(
userId = UserId(value = "@alice:server.org"), userId = UserId(value = "@alice:server.org"),
displayName = USER_NAME_ALICE, displayName = USER_NAME_ALICE,
avatarUrl = null, avatarUrl = null,
displayedStatus = DisplayedStatus.InCall(0L),
) )
fun aRoomMembersModerationState( fun aRoomMembersModerationState(
@@ -57,6 +57,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.matrix.ui.model.toText
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -264,9 +265,23 @@ private fun RoomMemberActionsBottomSheet(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) .padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
.fillMaxWidth() .fillMaxWidth()
) )
val userStatus = user.displayedStatus?.toText()
if (userStatus != null) {
Text(
text = userStatus,
style = ElementTheme.typography.fontBodyLgMedium,
maxLines = 1,
color = ElementTheme.colors.textSecondary,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
.fillMaxWidth()
)
}
// Show user ID only if it's different from the display name // Show user ID only if it's different from the display name
if (bestName != user.userId.value) { if (bestName != user.userId.value) {
Text( Text(
@@ -13,6 +13,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
data class UserProfileState( data class UserProfileState(
val userId: UserId, val userId: UserId,
@@ -26,6 +27,7 @@ data class UserProfileState(
val dmRoomId: RoomId?, val dmRoomId: RoomId?,
val canCall: Boolean, val canCall: Boolean,
val snackbarMessage: SnackbarMessage?, val snackbarMessage: SnackbarMessage?,
val displayedStatus: DisplayedStatus?,
val eventSink: (UserProfileEvents) -> Unit val eventSink: (UserProfileEvents) -> Unit
) { ) {
enum class ConfirmationDialog { enum class ConfirmationDialog {
@@ -145,6 +145,7 @@ class UserProfilePresenter(
userId = userId, userId = userId,
userName = userProfile?.displayName, userName = userProfile?.displayName,
avatarUrl = userProfile?.avatarUrl, avatarUrl = userProfile?.avatarUrl,
displayedStatus = userProfile?.displayedStatus,
isBlocked = isBlocked.value, isBlocked = isBlocked.value,
verificationState = UserProfileVerificationState.UNKNOWN, verificationState = UserProfileVerificationState.UNKNOWN,
startDmActionState = startDmActionState.value, startDmActionState = startDmActionState.value,
@@ -42,6 +42,8 @@ import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text 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.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.model.toText
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@@ -56,6 +58,7 @@ fun UserProfileHeaderSection(
openAvatarPreview: (url: String) -> Unit, openAvatarPreview: (url: String) -> Unit,
onUserIdClick: () -> Unit, onUserIdClick: () -> Unit,
withdrawVerificationClick: () -> Unit, withdrawVerificationClick: () -> Unit,
displayedStatus: DisplayedStatus?,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Column( Column(
@@ -90,7 +93,16 @@ fun UserProfileHeaderSection(
style = ElementTheme.typography.fontHeadingLgBold, style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(12.dp))
}
displayedStatus?.let { status ->
Text(
text = status.toText(),
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(12.dp))
} }
Text( Text(
modifier = Modifier.niceClickable { onUserIdClick() }, modifier = Modifier.niceClickable { onUserIdClick() },
@@ -99,6 +111,7 @@ fun UserProfileHeaderSection(
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
Spacer(modifier = Modifier.height(12.dp))
when (verificationState) { when (verificationState) {
UserProfileVerificationState.UNKNOWN, UserProfileVerificationState.UNVERIFIED -> Unit UserProfileVerificationState.UNKNOWN, UserProfileVerificationState.UNVERIFIED -> Unit
UserProfileVerificationState.VERIFIED -> { UserProfileVerificationState.VERIFIED -> {
@@ -143,6 +156,7 @@ internal fun UserProfileHeaderSectionPreview() = ElementPreview {
userId = UserId("@alice:example.com"), userId = UserId("@alice:example.com"),
userName = USER_NAME_ALICE, userName = USER_NAME_ALICE,
verificationState = UserProfileVerificationState.VERIFIED, verificationState = UserProfileVerificationState.VERIFIED,
displayedStatus = DisplayedStatus.InCall(0L),
openAvatarPreview = {}, openAvatarPreview = {},
onUserIdClick = {}, onUserIdClick = {},
withdrawVerificationClick = {}, withdrawVerificationClick = {},
@@ -157,6 +171,7 @@ internal fun UserProfileHeaderSectionWithVerificationViolationPreview() = Elemen
userId = UserId("@alice:example.com"), userId = UserId("@alice:example.com"),
userName = USER_NAME_ALICE, userName = USER_NAME_ALICE,
verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION, verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION,
displayedStatus = DisplayedStatus.InCall(0L),
openAvatarPreview = {}, openAvatarPreview = {},
onUserIdClick = {}, onUserIdClick = {},
withdrawVerificationClick = {}, withdrawVerificationClick = {},
@@ -18,6 +18,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> { open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> {
@@ -48,6 +49,7 @@ fun aUserProfileState(
dmRoomId: RoomId? = null, dmRoomId: RoomId? = null,
canCall: Boolean = false, canCall: Boolean = false,
snackbarMessage: SnackbarMessage? = null, snackbarMessage: SnackbarMessage? = null,
displayedStatus: DisplayedStatus? = null,
eventSink: (UserProfileEvents) -> Unit = {}, eventSink: (UserProfileEvents) -> Unit = {},
) = UserProfileState( ) = UserProfileState(
userId = userId, userId = userId,
@@ -61,5 +63,6 @@ fun aUserProfileState(
dmRoomId = dmRoomId, dmRoomId = dmRoomId,
canCall = canCall, canCall = canCall,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
displayedStatus = displayedStatus,
eventSink = eventSink, eventSink = eventSink,
) )
@@ -78,6 +78,7 @@ fun UserProfileView(
userId = state.userId, userId = state.userId,
userName = state.userName, userName = state.userName,
verificationState = state.verificationState, verificationState = state.verificationState,
displayedStatus = state.displayedStatus,
openAvatarPreview = { avatarUrl -> openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.userName ?: state.userId.value, avatarUrl) openAvatarPreview(state.userName ?: state.userId.value, avatarUrl)
}, },
+2 -2
View File
@@ -185,7 +185,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
# All new features should not be implemented in the pull request that upgrades the version, developers should # All new features should not be implemented in the pull request that upgrades the version, developers should
# only fix API breaks and may add some TODOs. # only fix API breaks and may add some TODOs.
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.07.22" matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.07.23"
# Others # Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@@ -230,7 +230,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
# Analytics # Analytics
posthog = "com.posthog:posthog-android:3.51.1" posthog = "com.posthog:posthog-android:3.51.1"
sentry = "io.sentry:sentry-android:8.48.0" sentry = "io.sentry:sentry-android:8.49.0"
# main branch can be tested replacing the version with main-SNAPSHOT # main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.37.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.37.0"
@@ -25,7 +25,7 @@ fun MatrixBadgeRowMolecule(
) { ) {
FlowRow( FlowRow(
modifier = modifier 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), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
@@ -1,17 +1,16 @@
/* /*
* Copyright (c) 2025 Element Creations Ltd. * Copyright (c) 2026 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
} }
android { android {
namespace = "io.element.android.libraries.recentemojis.api" namespace = "io.element.android.libraries.emoji.api"
} }
dependencies { dependencies {
@@ -19,5 +18,5 @@ dependencies {
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.collections.immutable)
implementation(libs.matrix.emojibase.bindings) api(libs.matrix.emojibase.bindings)
} }
@@ -0,0 +1,17 @@
/*
* 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.emoji.api.picker
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis
fun interface EmojiPickerPresenter : Presenter<EmojiPickerState> {
fun interface Factory {
fun create(getRecentEmojis: GetRecentEmojis): EmojiPickerPresenter
}
}
@@ -0,0 +1,44 @@
/*
* 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.emoji.api.picker
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import io.element.android.emojibasebindings.Emoji
import kotlinx.collections.immutable.ImmutableSet
@Immutable
interface EmojiPickerRenderer {
/**
* @param state opaque state produced by [EmojiPickerPresenter.present].
* @param onSelectEmoji invoked when the user taps an emoji (including a skin-tone variant).
* @param selectedEmojis emojis to visually mark as already selected.
* @param modifier layout modifier for the picker container.
* @param contentDescription accessibility label for each emoji cell.
*/
@Composable
fun Render(
state: EmojiPickerState,
onSelectEmoji: (Emoji) -> Unit,
selectedEmojis: ImmutableSet<String>,
modifier: Modifier = Modifier,
contentDescription: @Composable (Emoji, Boolean) -> String = { emoji, _ -> emoji.unicode },
)
}
object NoOpEmojiPickerRenderer : EmojiPickerRenderer {
@Composable
override fun Render(
state: EmojiPickerState,
onSelectEmoji: (Emoji) -> Unit,
selectedEmojis: ImmutableSet<String>,
modifier: Modifier,
contentDescription: @Composable (Emoji, Boolean) -> String,
) = Unit
}

Some files were not shown because too many files have changed in this diff Show More