Merge pull request #3259 from element-hq/feature/fga/pinned_message_banner_ui
WIP Pinned events : start creating the banner ui, no logic.
This commit is contained in:
commit
0bcfffdddf
29 changed files with 591 additions and 34 deletions
|
|
@ -41,6 +41,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
|
|||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
|
|
@ -101,6 +102,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
private val customReactionPresenter: CustomReactionPresenter,
|
||||
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
||||
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
|
||||
private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
|
|
@ -132,6 +134,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
val customReactionState = customReactionPresenter.present()
|
||||
val reactionSummaryState = reactionSummaryPresenter.present()
|
||||
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
|
||||
val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present()
|
||||
|
||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
|
||||
|
|
@ -230,6 +233,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
enableVoiceMessages = enableVoiceMessages,
|
||||
appName = buildMeta.applicationName,
|
||||
callState = callState,
|
||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl
|
|||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||
|
|
@ -54,6 +55,7 @@ data class MessagesState(
|
|||
val enableVoiceMessages: Boolean,
|
||||
val callState: RoomCallState,
|
||||
val appName: String,
|
||||
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
||||
val eventSink: (MessagesEvents) -> Unit
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import io.element.android.features.messages.impl.actionlist.anActionListState
|
|||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||
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.timeline.TimelineState
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||
|
|
@ -87,6 +89,12 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
|||
aMessagesState(
|
||||
callState = RoomCallState.DISABLED,
|
||||
),
|
||||
aMessagesState(
|
||||
pinnedMessagesBannerState = aPinnedMessagesBannerState(
|
||||
pinnedMessagesCount = 4,
|
||||
currentPinnedMessageIndex = 0,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +121,7 @@ fun aMessagesState(
|
|||
showReinvitePrompt: Boolean = false,
|
||||
enableVoiceMessages: Boolean = true,
|
||||
callState: RoomCallState = RoomCallState.ENABLED,
|
||||
pinnedMessagesBannerState: PinnedMessagesBannerState = aPinnedMessagesBannerState(),
|
||||
eventSink: (MessagesEvents) -> Unit = {},
|
||||
) = MessagesState(
|
||||
roomId = RoomId("!id:domain"),
|
||||
|
|
@ -136,6 +145,7 @@ fun aMessagesState(
|
|||
enableVoiceMessages = enableVoiceMessages,
|
||||
callState = callState,
|
||||
appName = "Element",
|
||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
package io.element.android.features.messages.impl
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -33,6 +36,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -68,6 +72,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.PinnedMessagesBannerView
|
||||
import io.element.android.features.messages.impl.timeline.TimelineView
|
||||
import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
||||
|
|
@ -100,6 +105,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.KeepScreenOn
|
||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||
import io.element.android.libraries.designsystem.utils.isScrollingUp
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -370,22 +376,34 @@ private fun MessagesViewContent(
|
|||
RectangleShape
|
||||
},
|
||||
content = { paddingValues ->
|
||||
TimelineView(
|
||||
state = state.timelineState,
|
||||
typingNotificationState = state.typingNotificationState,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onMessageClick = onMessageClick,
|
||||
onMessageLongClick = onMessageLongClick,
|
||||
onSwipeToReply = onSwipeToReply,
|
||||
onReactionClick = onReactionClick,
|
||||
onReactionLongClick = onReactionLongClick,
|
||||
onMoreReactionsClick = onMoreReactionsClick,
|
||||
onReadReceiptClick = onReadReceiptClick,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
)
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
val timelineLazyListState = rememberLazyListState()
|
||||
TimelineView(
|
||||
state = state.timelineState,
|
||||
typingNotificationState = state.typingNotificationState,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onMessageClick = onMessageClick,
|
||||
onMessageLongClick = onMessageLongClick,
|
||||
onSwipeToReply = onSwipeToReply,
|
||||
onReactionClick = onReactionClick,
|
||||
onReactionLongClick = onReactionLongClick,
|
||||
onMoreReactionsClick = onMoreReactionsClick,
|
||||
onReadReceiptClick = onReadReceiptClick,
|
||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
lazyListState = timelineLazyListState,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = state.pinnedMessagesBannerState.displayBanner && timelineLazyListState.isScrollingUp(),
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically(),
|
||||
) {
|
||||
PinnedMessagesBannerView(
|
||||
state = state.pinnedMessagesBannerState,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
sheetContent = { subcomposing: Boolean ->
|
||||
MessagesViewComposerBottomSheetContents(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
|
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesTo(SessionScope::class)
|
||||
@Module
|
||||
interface MessagesModule {
|
||||
@Binds
|
||||
fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter<PinnedMessagesBannerState>
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
sealed interface PinnedMessagesBannerEvents {
|
||||
data object MoveToNextPinned : PinnedMessagesBannerEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import javax.inject.Inject
|
||||
|
||||
class PinnedMessagesBannerPresenter @Inject constructor() : Presenter<PinnedMessagesBannerState> {
|
||||
@Composable
|
||||
override fun present(): PinnedMessagesBannerState {
|
||||
var pinnedMessageCount by remember {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
var currentPinnedMessageIndex by rememberSaveable {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
fun handleEvent(event: PinnedMessagesBannerEvents) {
|
||||
when (event) {
|
||||
is PinnedMessagesBannerEvents.MoveToNextPinned -> {
|
||||
if (currentPinnedMessageIndex < pinnedMessageCount - 1) {
|
||||
currentPinnedMessageIndex++
|
||||
} else {
|
||||
currentPinnedMessageIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PinnedMessagesBannerState(
|
||||
pinnedMessagesCount = pinnedMessageCount,
|
||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||
eventSink = ::handleEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
data class PinnedMessagesBannerState(
|
||||
val pinnedMessagesCount: Int,
|
||||
val currentPinnedMessageIndex: Int,
|
||||
val eventSink: (PinnedMessagesBannerEvents) -> Unit
|
||||
) {
|
||||
val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessageIndex < pinnedMessagesCount
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider<PinnedMessagesBannerState> {
|
||||
override val values: Sequence<PinnedMessagesBannerState>
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aPinnedMessagesBannerState(
|
||||
pinnedMessagesCount: Int = 0,
|
||||
currentPinnedMessageIndex: Int = -1,
|
||||
eventSink: (PinnedMessagesBannerEvents) -> Unit = {}
|
||||
) = PinnedMessagesBannerState(
|
||||
pinnedMessagesCount = pinnedMessagesCount,
|
||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
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.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
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.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.pinnedMessageBannerBorder
|
||||
import io.element.android.libraries.designsystem.theme.pinnedMessageBannerIndicator
|
||||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun PinnedMessagesBannerView(
|
||||
state: PinnedMessagesBannerState,
|
||||
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 {
|
||||
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = spacedBy(10.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
PinIndicators(
|
||||
pinIndex = state.currentPinnedMessageIndex,
|
||||
pinsCount = state.pinnedMessagesCount,
|
||||
modifier = Modifier.heightIn(max = 40.dp)
|
||||
)
|
||||
Icon(
|
||||
imageVector = CompoundIcons.PinSolid(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
PinnedMessageItem(
|
||||
index = state.currentPinnedMessageIndex,
|
||||
totalCount = state.pinnedMessagesCount,
|
||||
message = "This is a pinned message",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
TextButton(text = stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title), onClick = { /*TODO*/ })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinIndicators(
|
||||
pinIndex: Int,
|
||||
pinsCount: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val indicatorHeight by remember {
|
||||
derivedStateOf {
|
||||
when (pinsCount) {
|
||||
0 -> 0
|
||||
1 -> 32
|
||||
2 -> 18
|
||||
else -> 11
|
||||
}
|
||||
}
|
||||
}
|
||||
val lazyListState = rememberLazyListState()
|
||||
LaunchedEffect(pinIndex) {
|
||||
val viewportSize = lazyListState.layoutInfo.viewportSize
|
||||
lazyListState.animateScrollToItem(
|
||||
pinIndex,
|
||||
indicatorHeight / 2 - viewportSize.height / 2
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = lazyListState,
|
||||
verticalArrangement = spacedBy(2.dp),
|
||||
userScrollEnabled = false
|
||||
) {
|
||||
items(pinsCount) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
.height(indicatorHeight.dp)
|
||||
.background(
|
||||
color = if (index == pinIndex) {
|
||||
ElementTheme.colors.iconAccentPrimary
|
||||
} else {
|
||||
ElementTheme.colors.pinnedMessageBannerIndicator
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinnedMessageItem(
|
||||
index: Int,
|
||||
totalCount: Int,
|
||||
message: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val countMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator, index + 1, totalCount)
|
||||
val fullCountMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator_description, countMessage)
|
||||
Column(modifier = modifier) {
|
||||
if (totalCount > 1) {
|
||||
Text(
|
||||
text = annotatedTextWithBold(
|
||||
text = fullCountMessage,
|
||||
boldText = countMessage,
|
||||
),
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
color = ElementTheme.colors.textActionAccent,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = message,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun PinnedMessagesBannerViewPreview(@PreviewParameter(PinnedMessagesBannerStateProvider::class) state: PinnedMessagesBannerState) = ElementPreview {
|
||||
PinnedMessagesBannerView(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
|
@ -90,6 +90,7 @@ fun TimelineView(
|
|||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
lazyListState: LazyListState = rememberLazyListState(),
|
||||
forceJumpToBottomVisibility: Boolean = false
|
||||
) {
|
||||
fun clearFocusRequestState() {
|
||||
|
|
@ -109,7 +110,6 @@ fun TimelineView(
|
|||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val lazyListState = rememberLazyListState()
|
||||
// Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version
|
||||
val useReverseLayout = remember {
|
||||
val accessibilityManager = context.getSystemService(AccessibilityManager::class.java)
|
||||
|
|
|
|||
|
|
@ -30,6 +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.textcomposer.TestRichTextEditorStateFactory
|
||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
||||
|
|
@ -158,7 +159,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
assertThat(room.markAsReadCalls).isEmpty()
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
|
@ -179,7 +180,7 @@ class MessagesPresenterTest {
|
|||
canRedactOtherResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
).apply {
|
||||
).apply {
|
||||
givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||
}
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
|
|
@ -208,7 +209,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -246,7 +247,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -305,7 +306,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(
|
||||
clipboardHelper = clipboardHelper,
|
||||
matrixRoom = matrixRoom,
|
||||
|
|
@ -495,7 +496,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
|
||||
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
|
||||
liveTimeline.redactEventLambda = redactEventLambda
|
||||
|
|
@ -570,7 +571,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -606,7 +607,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -631,7 +632,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -656,7 +657,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
room.givenRoomMembersState(
|
||||
MatrixRoomMembersState.Ready(
|
||||
persistentListOf(
|
||||
|
|
@ -692,7 +693,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
room.givenRoomMembersState(
|
||||
MatrixRoomMembersState.Error(
|
||||
failure = Throwable(),
|
||||
|
|
@ -729,7 +730,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
|
@ -756,7 +757,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
room.givenRoomMembersState(
|
||||
MatrixRoomMembersState.Ready(
|
||||
persistentListOf(
|
||||
|
|
@ -797,7 +798,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -822,7 +823,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -844,7 +845,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -865,7 +866,7 @@ class MessagesPresenterTest {
|
|||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -1060,6 +1061,7 @@ class MessagesPresenterTest {
|
|||
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
||||
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||
|
||||
return MessagesPresenter(
|
||||
room = matrixRoom,
|
||||
composerPresenter = messageComposerPresenter,
|
||||
|
|
@ -1070,6 +1072,7 @@ class MessagesPresenterTest {
|
|||
customReactionPresenter = customReactionPresenter,
|
||||
reactionSummaryPresenter = reactionSummaryPresenter,
|
||||
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
|
||||
pinnedMessagesBannerPresenter = { aPinnedMessagesBannerState() },
|
||||
networkMonitor = FakeNetworkMonitor(),
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
navigator = navigator,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class PinnedMessagesBannerPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPinnedMessagesBannerPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.pinnedMessagesCount).isEqualTo(0)
|
||||
assertThat(initialState.currentPinnedMessageIndex).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - move to next pinned message when there is no pinned events`() = runTest {
|
||||
val presenter = createPinnedMessagesBannerPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||
// Nothing is emitted
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPinnedMessagesBannerPresenter(): PinnedMessagesBannerPresenter {
|
||||
return PinnedMessagesBannerPresenter()
|
||||
}
|
||||
}
|
||||
|
|
@ -179,6 +179,14 @@ val SemanticColors.badgeNegativeBackgroundColor
|
|||
val SemanticColors.badgeNegativeContentColor
|
||||
get() = if (isLight) LightColorTokens.colorRed1100 else DarkColorTokens.colorRed1100
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.pinnedMessageBannerIndicator
|
||||
get() = if (isLight) LightColorTokens.colorAlphaGray600 else DarkColorTokens.colorAlphaGray600
|
||||
|
||||
@OptIn(CoreColorToken::class)
|
||||
val SemanticColors.pinnedMessageBannerBorder
|
||||
get() = if (isLight) LightColorTokens.colorAlphaGray400 else DarkColorTokens.colorAlphaGray400
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ColorAliasesPreview() = ElementPreview {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.utils
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
/**
|
||||
* Returns whether the lazy list is currently scrolling up.
|
||||
*/
|
||||
@Composable
|
||||
fun LazyListState.isScrollingUp(): Boolean {
|
||||
var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) }
|
||||
var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) }
|
||||
return remember(this) {
|
||||
derivedStateOf {
|
||||
if (previousIndex != firstVisibleItemIndex) {
|
||||
previousIndex > firstVisibleItemIndex
|
||||
} else {
|
||||
previousScrollOffset >= firstVisibleItemScrollOffset
|
||||
}.also {
|
||||
previousIndex = firstVisibleItemIndex
|
||||
previousScrollOffset = firstVisibleItemScrollOffset
|
||||
}
|
||||
}
|
||||
}.value
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7114202a1de9860547c525c0dadc110ce9e2e198465218ac2c33cf65f2f0eaa2
|
||||
size 9496
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8e6b5fd9ecc2b01cc8a83f3fe8e34352de1792a82db85c396377a18246adad1a
|
||||
size 12953
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:14ca5901134299e801e204e280d731e7de4072f1d522b076eb41c5f806897ed2
|
||||
size 12905
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bb206284c642dd665290d5d553491e622b8e15a64df7bb2dbd91ea5d3a13e19a
|
||||
size 13041
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a1940c4ee1e07c6a0198682460af1a6558fcaf14cb69ff061831cb591eb7aec3
|
||||
size 13066
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fee5c1dbcdf7929f4762b2915584fe45b7f39916a949663f03e8d7e85e991b4b
|
||||
size 12988
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:16658495b889654f152ba80a52111164f9682009b96abf1f3f20e660bd7c2407
|
||||
size 9297
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43651c4a7f10406f1a6a4b995467ba292d4a43bc198dcf352ae2e66694154de2
|
||||
size 12340
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cca8ebb1ec12497de7e2efc1725a2e4427eecd1d340ae8176d10f914def0af25
|
||||
size 12297
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:37bf00fbf548b7ba3d601af1ca489e07e25059c2f5a68abf9b85f4c656cf482c
|
||||
size 12425
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed813a7003eb01a06667b10191990ed5bb3f75ee6a447cc4d52510b7e13b3724
|
||||
size 12448
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6fa556ff7f6757c69c24e47520e08ccfd1b009d8e49a704c36d7fc4ca4186cbf
|
||||
size 12378
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:35761e520dcd7ad01d3913a98479314a1df645bdf141214e3a180fe150d2e8fd
|
||||
size 59959
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:acd2cf24e0a894936d6ed0896fb4ebcdba3529437e595ca54aa13a6f8ee79a5f
|
||||
size 59327
|
||||
Loading…
Add table
Add a link
Reference in a new issue