diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index a1b7bc6ad1..97436c2bc0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -23,7 +23,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState -import io.element.android.features.messages.impl.pinned.banner.aPinnedMessagesBannerState +import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineState @@ -90,8 +90,8 @@ open class MessagesStateProvider : PreviewParameterProvider { callState = RoomCallState.DISABLED, ), aMessagesState( - pinnedMessagesBannerState = aPinnedMessagesBannerState( - pinnedMessagesCount = 4, + pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState( + knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 0, ), ), @@ -121,7 +121,7 @@ fun aMessagesState( showReinvitePrompt: Boolean = false, enableVoiceMessages: Boolean = true, callState: RoomCallState = RoomCallState.ENABLED, - pinnedMessagesBannerState: PinnedMessagesBannerState = aPinnedMessagesBannerState(), + pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(), eventSink: (MessagesEvents) -> Unit = {}, ) = MessagesState( roomId = RoomId("!id:domain"), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index d983404f9b..f365d917f0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -71,6 +71,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsBott import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerView +import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerViewDefaults import io.element.android.features.messages.impl.timeline.TimelineEvents @@ -400,7 +401,7 @@ private fun MessagesViewContent( nestedScrollConnection = scrollBehavior.nestedScrollConnection, ) AnimatedVisibility( - visible = state.pinnedMessagesBannerState.displayBanner && scrollBehavior.isVisible, + visible = state.pinnedMessagesBannerState != PinnedMessagesBannerState.Hidden && scrollBehavior.isVisible, enter = expandVertically(), exit = shrinkVertically(), ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt index 16b8c97af7..59bf1c5a2f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt @@ -26,10 +26,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn @@ -43,29 +47,34 @@ class PinnedMessagesBannerPresenter @Inject constructor( private val room: MatrixRoom, private val itemFactory: PinnedMessagesBannerItemFactory, private val featureFlagService: FeatureFlagService, + private val networkMonitor: NetworkMonitor, ) : Presenter { @Composable override fun present(): PinnedMessagesBannerState { + val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false) + var timelineFailed by rememberSaveable { mutableStateOf(false) } var pinnedItems by remember { - mutableStateOf>(emptyList()) + mutableStateOf>(persistentListOf()) } + val knownPinnedMessagesCount by remember { + room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size } + }.collectAsState(initial = null) - fun onItemsChange(newItems: List) { - pinnedItems = newItems - } + var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(0) } - var currentPinnedMessageIndex by rememberSaveable { - mutableIntStateOf(0) - } - - LaunchedEffect(pinnedItems) { - val pinnedMessageCount = pinnedItems.size - if (currentPinnedMessageIndex >= pinnedMessageCount) { - currentPinnedMessageIndex = (pinnedMessageCount - 1).coerceAtLeast(0) + PinnedMessagesBannerItemsEffect( + isFeatureEnabled = isFeatureEnabled, + onItemsChange = { newItems -> + val pinnedMessageCount = newItems.size + if (currentPinnedMessageIndex >= pinnedMessageCount) { + currentPinnedMessageIndex = 0 + } + pinnedItems = newItems + }, + onTimelineFail = { hasTimelineFailed -> + timelineFailed = hasTimelineFailed } - } - - PinnedMessagesBannerItemsEffect(::onItemsChange) + ) fun handleEvent(event: PinnedMessagesBannerEvents) { when (event) { @@ -79,32 +88,68 @@ class PinnedMessagesBannerPresenter @Inject constructor( } } - return PinnedMessagesBannerState( - pinnedMessagesCount = pinnedItems.size, - currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex), + return pinnedMessagesBannerState( + isFeatureEnabled = isFeatureEnabled, + hasTimelineFailed = timelineFailed, + realPinnedMessagesCount = knownPinnedMessagesCount, + pinnedItems = pinnedItems, currentPinnedMessageIndex = currentPinnedMessageIndex, eventSink = ::handleEvent ) } + @Composable + private fun pinnedMessagesBannerState( + isFeatureEnabled: Boolean, + hasTimelineFailed: Boolean, + realPinnedMessagesCount: Int?, + pinnedItems: ImmutableList, + currentPinnedMessageIndex: Int, + eventSink: (PinnedMessagesBannerEvents) -> Unit + ): PinnedMessagesBannerState { + val currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex) + return when { + !isFeatureEnabled -> PinnedMessagesBannerState.Hidden + hasTimelineFailed -> PinnedMessagesBannerState.Hidden + realPinnedMessagesCount == null || realPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden + currentPinnedMessage == null -> PinnedMessagesBannerState.Loading(realPinnedMessagesCount = realPinnedMessagesCount) + else -> { + PinnedMessagesBannerState.Loaded( + currentPinnedMessage = currentPinnedMessage, + currentPinnedMessageIndex = currentPinnedMessageIndex, + knownPinnedMessagesCount = realPinnedMessagesCount, + eventSink = eventSink + ) + } + } + } + @OptIn(FlowPreview::class) @Composable private fun PinnedMessagesBannerItemsEffect( - onItemsChange: (List) -> Unit, + isFeatureEnabled: Boolean, + onItemsChange: (ImmutableList) -> Unit, + onTimelineFail: (Boolean) -> Unit, ) { - val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false) val updatedOnItemsChange by rememberUpdatedState(onItemsChange) + val updatedOnTimelineFail by rememberUpdatedState(onTimelineFail) + val networkStatus by networkMonitor.connectivity.collectAsState() - LaunchedEffect(isFeatureEnabled) { + LaunchedEffect(isFeatureEnabled, networkStatus) { if (!isFeatureEnabled) return@LaunchedEffect - val pinnedEventsTimeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect + val pinnedEventsTimeline = room.pinnedEventsTimeline() + .onFailure { updatedOnTimelineFail(true) } + .onSuccess { updatedOnTimelineFail(false) } + .getOrNull() + ?: return@LaunchedEffect + pinnedEventsTimeline.timelineItems .debounce(300.milliseconds) .map { timelineItems -> timelineItems.mapNotNull { timelineItem -> itemFactory.create(timelineItem) - } + }.toImmutableList() } .onEach { newItems -> updatedOnItemsChange(newItems) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt index b607855058..886cef81de 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt @@ -16,11 +16,40 @@ package io.element.android.features.messages.impl.pinned.banner -data class PinnedMessagesBannerState( - val pinnedMessagesCount: Int, - val currentPinnedMessageIndex: Int, - val currentPinnedMessage: PinnedMessagesBannerItem?, - val eventSink: (PinnedMessagesBannerEvents) -> Unit -) { - val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessage != null +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import io.element.android.libraries.designsystem.text.toAnnotatedString +import io.element.android.libraries.ui.strings.CommonStrings + +@Immutable +sealed interface PinnedMessagesBannerState { + data object Hidden : PinnedMessagesBannerState + data class Loading(val realPinnedMessagesCount: Int) : PinnedMessagesBannerState + data class Loaded( + val currentPinnedMessage: PinnedMessagesBannerItem, + val currentPinnedMessageIndex: Int, + val knownPinnedMessagesCount: Int, + val eventSink: (PinnedMessagesBannerEvents) -> Unit + ) : PinnedMessagesBannerState + + fun pinnedMessagesCount() = when (this) { + is Hidden -> 0 + is Loading -> realPinnedMessagesCount + is Loaded -> knownPinnedMessagesCount + } + + fun currentPinnedMessageIndex() = when (this) { + is Hidden -> 0 + is Loading -> 0 + is Loaded -> currentPinnedMessageIndex + } + + @Composable + fun formattedMessage() = when (this) { + is Hidden -> AnnotatedString("") + is Loading -> stringResource(id = CommonStrings.screen_room_pinned_banner_loading_description).toAnnotatedString() + is Loaded -> currentPinnedMessage.formatted + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStateProvider.kt index dcd6a4984a..bdcab879fe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStateProvider.kt @@ -24,26 +24,38 @@ import kotlin.random.Random internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aPinnedMessagesBannerState(pinnedMessagesCount = 1, currentPinnedMessageIndex = 0), - aPinnedMessagesBannerState(pinnedMessagesCount = 2, currentPinnedMessageIndex = 0), - aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 0), - aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 1), - aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 2), - aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 3), + aHiddenPinnedMessagesBannerState(), + aLoadingPinnedMessagesBannerState(knownPinnedMessagesCount = 1), + aLoadingPinnedMessagesBannerState(knownPinnedMessagesCount = 4), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 1, currentPinnedMessageIndex = 0), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 2, currentPinnedMessageIndex = 0), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 3, currentPinnedMessageIndex = 0), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 0), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 1), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 2), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 3), ) } -internal fun aPinnedMessagesBannerState( - pinnedMessagesCount: Int = 0, - currentPinnedMessageIndex: Int = -1, +internal fun aHiddenPinnedMessagesBannerState() = PinnedMessagesBannerState.Hidden + +internal fun aLoadingPinnedMessagesBannerState( + knownPinnedMessagesCount: Int = 4 +) = PinnedMessagesBannerState.Loading( + realPinnedMessagesCount = knownPinnedMessagesCount +) + +internal fun aLoadedPinnedMessagesBannerState( + currentPinnedMessageIndex: Int = 0, + knownPinnedMessagesCount: Int = 1, currentPinnedMessage: PinnedMessagesBannerItem = PinnedMessagesBannerItem( eventId = EventId("\$" + Random.nextInt().toString()), formatted = AnnotatedString("This is a pinned message") ), eventSink: (PinnedMessagesBannerEvents) -> Unit = {} -) = PinnedMessagesBannerState( - pinnedMessagesCount = pinnedMessagesCount, - currentPinnedMessageIndex = currentPinnedMessageIndex, +) = PinnedMessagesBannerState.Loaded( currentPinnedMessage = currentPinnedMessage, + currentPinnedMessageIndex = currentPinnedMessageIndex, + knownPinnedMessagesCount = knownPinnedMessagesCount, eventSink = eventSink ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt index 0ddbe1ed7b..b7524e43df 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt @@ -71,42 +71,52 @@ fun PinnedMessagesBannerView( onViewAllClick: () -> Unit, modifier: Modifier = Modifier, ) { - if (state.currentPinnedMessage == null) return + Box(modifier = modifier) { + when (state) { + PinnedMessagesBannerState.Hidden -> Unit + is PinnedMessagesBannerState.Loading -> { + PinnedMessagesBannerRow( + state = state, + onViewAllClick = onViewAllClick, + modifier = Modifier.clickable(onClick = { }), + ) + } + is PinnedMessagesBannerState.Loaded -> { + fun onClick() { + onClick(state.currentPinnedMessage.eventId) + state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) + } + PinnedMessagesBannerRow( + state = state, + onViewAllClick = onViewAllClick, + modifier = Modifier.clickable(onClick = ::onClick), + ) + } + } + } +} + +@Composable +fun PinnedMessagesBannerRow( + state: PinnedMessagesBannerState, + onViewAllClick: () -> Unit, + modifier: Modifier = Modifier, +) { val borderColor = ElementTheme.colors.pinnedMessageBannerBorder Row( modifier = modifier - .background(color = ElementTheme.colors.bgCanvasDefault) - .fillMaxWidth() - .drawBehind { - val strokeWidth = 0.5.dp.toPx() - val y = size.height - strokeWidth / 2 - drawLine( - borderColor, - Offset(0f, y), - Offset(size.width, y), - strokeWidth - ) - drawLine( - borderColor, - Offset(0f, 0f), - Offset(size.width, 0f), - strokeWidth - ) - } - .shadow(elevation = 5.dp, spotColor = Color.Transparent) - .heightIn(min = 64.dp) - .clickable { - onClick(state.currentPinnedMessage.eventId) - state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) - }, + .background(color = ElementTheme.colors.bgCanvasDefault) + .fillMaxWidth() + .drawBorder(borderColor) + .heightIn(min = 64.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = spacedBy(10.dp) ) { Spacer(modifier = Modifier.width(16.dp)) PinIndicators( - pinIndex = state.currentPinnedMessageIndex, - pinsCount = state.pinnedMessagesCount, + pinIndex = state.currentPinnedMessageIndex(), + pinsCount = state.pinnedMessagesCount(), modifier = Modifier.heightIn(max = 40.dp) ) Icon( @@ -116,15 +126,56 @@ fun PinnedMessagesBannerView( modifier = Modifier.size(20.dp) ) PinnedMessageItem( - index = state.currentPinnedMessageIndex, - totalCount = state.pinnedMessagesCount, - message = state.currentPinnedMessage.formatted, + index = state.currentPinnedMessageIndex(), + totalCount = state.pinnedMessagesCount(), + message = state.formattedMessage(), modifier = Modifier.weight(1f) ) - TextButton(text = stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title), onClick = onViewAllClick) + ViewAllButton(state, onViewAllClick) } } +@Composable +private fun ViewAllButton( + state: PinnedMessagesBannerState, + onViewAllClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + val text = if (state is PinnedMessagesBannerState.Loaded) { + stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title) + } else { + "" + } + TextButton( + text = text, + showProgress = state is PinnedMessagesBannerState.Loading, + onClick = onViewAllClick + ) + } +} + +private fun Modifier.drawBorder(borderColor: Color): Modifier { + return this + .drawBehind { + val strokeWidth = 0.5.dp.toPx() + val y = size.height - strokeWidth / 2 + drawLine( + borderColor, + Offset(0f, y), + Offset(size.width, y), + strokeWidth + ) + drawLine( + borderColor, + Offset(0f, 0f), + Offset(size.width, 0f), + strokeWidth + ) + } + .shadow(elevation = 5.dp, spotColor = Color.Transparent) +} + @Composable private fun PinIndicators( pinIndex: Int, @@ -157,15 +208,15 @@ private fun PinIndicators( items(pinsCount) { index -> Box( modifier = Modifier - .width(2.dp) - .height(indicatorHeight.dp) - .background( - color = if (index == pinIndex) { - ElementTheme.colors.iconAccentPrimary - } else { - ElementTheme.colors.pinnedMessageBannerIndicator - } - ) + .width(2.dp) + .height(indicatorHeight.dp) + .background( + color = if (index == pinIndex) { + ElementTheme.colors.iconAccentPrimary + } else { + ElementTheme.colors.pinnedMessageBannerIndicator + } + ) ) } } @@ -175,7 +226,7 @@ private fun PinIndicators( private fun PinnedMessageItem( index: Int, totalCount: Int, - message: AnnotatedString, + message: AnnotatedString?, modifier: Modifier = Modifier, ) { val countMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator, index + 1, totalCount) @@ -193,13 +244,15 @@ private fun PinnedMessageItem( overflow = TextOverflow.Ellipsis, ) } - Text( - text = message, - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textPrimary, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) + if (message != null) { + Text( + text = message, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textPrimary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index d04c0974e0..a6c123971b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -30,7 +30,7 @@ import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter -import io.element.android.features.messages.impl.pinned.banner.aPinnedMessagesBannerState +import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineItemIndexer @@ -1072,7 +1072,7 @@ class MessagesPresenterTest { customReactionPresenter = customReactionPresenter, reactionSummaryPresenter = reactionSummaryPresenter, readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter, - pinnedMessagesBannerPresenter = { aPinnedMessagesBannerState() }, + pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), navigator = navigator, diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 5766c900dd..c66d504ea8 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -293,6 +293,7 @@ Reason: %1$s." "Unblock user" "%1$s of %2$s" "%1$s Pinned messages" + "Loading message…" "View All" "Chat" "Share location"