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
4ac288aa3c
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.MessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
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.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.TimelineController
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
|
|
@ -101,6 +102,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val customReactionPresenter: CustomReactionPresenter,
|
private val customReactionPresenter: CustomReactionPresenter,
|
||||||
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
||||||
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
|
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
|
||||||
|
private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
|
@ -132,6 +134,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
val customReactionState = customReactionPresenter.present()
|
val customReactionState = customReactionPresenter.present()
|
||||||
val reactionSummaryState = reactionSummaryPresenter.present()
|
val reactionSummaryState = reactionSummaryPresenter.present()
|
||||||
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
|
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
|
||||||
|
val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present()
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
|
|
||||||
|
|
@ -230,6 +233,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
enableVoiceMessages = enableVoiceMessages,
|
enableVoiceMessages = enableVoiceMessages,
|
||||||
appName = buildMeta.applicationName,
|
appName = buildMeta.applicationName,
|
||||||
callState = callState,
|
callState = callState,
|
||||||
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
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.messagecomposer.MessageComposerState
|
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.TimelineState
|
||||||
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.ReactionSummaryState
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||||
|
|
@ -54,6 +55,7 @@ data class MessagesState(
|
||||||
val enableVoiceMessages: Boolean,
|
val enableVoiceMessages: Boolean,
|
||||||
val callState: RoomCallState,
|
val callState: RoomCallState,
|
||||||
val appName: String,
|
val appName: String,
|
||||||
|
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
||||||
val eventSink: (MessagesEvents) -> Unit
|
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.AttachmentsState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
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.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.TimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||||
|
|
@ -87,6 +89,12 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
aMessagesState(
|
aMessagesState(
|
||||||
callState = RoomCallState.DISABLED,
|
callState = RoomCallState.DISABLED,
|
||||||
),
|
),
|
||||||
|
aMessagesState(
|
||||||
|
pinnedMessagesBannerState = aPinnedMessagesBannerState(
|
||||||
|
pinnedMessagesCount = 4,
|
||||||
|
currentPinnedMessageIndex = 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,6 +121,7 @@ fun aMessagesState(
|
||||||
showReinvitePrompt: Boolean = false,
|
showReinvitePrompt: Boolean = false,
|
||||||
enableVoiceMessages: Boolean = true,
|
enableVoiceMessages: Boolean = true,
|
||||||
callState: RoomCallState = RoomCallState.ENABLED,
|
callState: RoomCallState = RoomCallState.ENABLED,
|
||||||
|
pinnedMessagesBannerState: PinnedMessagesBannerState = aPinnedMessagesBannerState(),
|
||||||
eventSink: (MessagesEvents) -> Unit = {},
|
eventSink: (MessagesEvents) -> Unit = {},
|
||||||
) = MessagesState(
|
) = MessagesState(
|
||||||
roomId = RoomId("!id:domain"),
|
roomId = RoomId("!id:domain"),
|
||||||
|
|
@ -136,6 +145,7 @@ fun aMessagesState(
|
||||||
enableVoiceMessages = enableVoiceMessages,
|
enableVoiceMessages = enableVoiceMessages,
|
||||||
callState = callState,
|
callState = callState,
|
||||||
appName = "Element",
|
appName = "Element",
|
||||||
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl
|
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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
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.AttachmentsState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
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.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.TimelineView
|
||||||
import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem
|
import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
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.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.designsystem.utils.KeepScreenOn
|
import io.element.android.libraries.designsystem.utils.KeepScreenOn
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
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.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -370,22 +376,34 @@ private fun MessagesViewContent(
|
||||||
RectangleShape
|
RectangleShape
|
||||||
},
|
},
|
||||||
content = { paddingValues ->
|
content = { paddingValues ->
|
||||||
TimelineView(
|
Box(modifier = Modifier.padding(paddingValues)) {
|
||||||
state = state.timelineState,
|
val timelineLazyListState = rememberLazyListState()
|
||||||
typingNotificationState = state.typingNotificationState,
|
TimelineView(
|
||||||
onUserDataClick = onUserDataClick,
|
state = state.timelineState,
|
||||||
onLinkClick = onLinkClick,
|
typingNotificationState = state.typingNotificationState,
|
||||||
onMessageClick = onMessageClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onMessageLongClick = onMessageLongClick,
|
onLinkClick = onLinkClick,
|
||||||
onSwipeToReply = onSwipeToReply,
|
onMessageClick = onMessageClick,
|
||||||
onReactionClick = onReactionClick,
|
onMessageLongClick = onMessageLongClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onSwipeToReply = onSwipeToReply,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onReactionClick = onReactionClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
modifier = Modifier.padding(paddingValues),
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onJoinCallClick = onJoinCallClick,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
)
|
onJoinCallClick = onJoinCallClick,
|
||||||
|
lazyListState = timelineLazyListState,
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.pinnedMessagesBannerState.displayBanner && timelineLazyListState.isScrollingUp(),
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically(),
|
||||||
|
) {
|
||||||
|
PinnedMessagesBannerView(
|
||||||
|
state = state.pinnedMessagesBannerState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
sheetContent = { subcomposing: Boolean ->
|
sheetContent = { subcomposing: Boolean ->
|
||||||
MessagesViewComposerBottomSheetContents(
|
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,
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
lazyListState: LazyListState = rememberLazyListState(),
|
||||||
forceJumpToBottomVisibility: Boolean = false
|
forceJumpToBottomVisibility: Boolean = false
|
||||||
) {
|
) {
|
||||||
fun clearFocusRequestState() {
|
fun clearFocusRequestState() {
|
||||||
|
|
@ -109,7 +110,6 @@ fun TimelineView(
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
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
|
// Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version
|
||||||
val useReverseLayout = remember {
|
val useReverseLayout = remember {
|
||||||
val accessibilityManager = context.getSystemService(AccessibilityManager::class.java)
|
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.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext
|
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.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.textcomposer.TestRichTextEditorStateFactory
|
||||||
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.TimelineItemIndexer
|
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
||||||
|
|
@ -158,7 +159,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
assertThat(room.markAsReadCalls).isEmpty()
|
assertThat(room.markAsReadCalls).isEmpty()
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -179,7 +180,7 @@ class MessagesPresenterTest {
|
||||||
canRedactOtherResult = { Result.success(true) },
|
canRedactOtherResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
).apply {
|
).apply {
|
||||||
givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
givenRoomInfo(aRoomInfo(hasRoomCall = true))
|
||||||
}
|
}
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
|
|
@ -208,7 +209,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -246,7 +247,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -305,7 +306,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(
|
val presenter = createMessagesPresenter(
|
||||||
clipboardHelper = clipboardHelper,
|
clipboardHelper = clipboardHelper,
|
||||||
matrixRoom = matrixRoom,
|
matrixRoom = matrixRoom,
|
||||||
|
|
@ -495,7 +496,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
|
|
||||||
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
|
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) }
|
||||||
liveTimeline.redactEventLambda = redactEventLambda
|
liveTimeline.redactEventLambda = redactEventLambda
|
||||||
|
|
@ -570,7 +571,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -606,7 +607,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -631,7 +632,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -656,7 +657,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
room.givenRoomMembersState(
|
room.givenRoomMembersState(
|
||||||
MatrixRoomMembersState.Ready(
|
MatrixRoomMembersState.Ready(
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
|
|
@ -692,7 +693,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
room.givenRoomMembersState(
|
room.givenRoomMembersState(
|
||||||
MatrixRoomMembersState.Error(
|
MatrixRoomMembersState.Error(
|
||||||
failure = Throwable(),
|
failure = Throwable(),
|
||||||
|
|
@ -729,7 +730,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
|
room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -756,7 +757,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
room.givenRoomMembersState(
|
room.givenRoomMembersState(
|
||||||
MatrixRoomMembersState.Ready(
|
MatrixRoomMembersState.Ready(
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
|
|
@ -797,7 +798,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -822,7 +823,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -844,7 +845,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -865,7 +866,7 @@ class MessagesPresenterTest {
|
||||||
canUserJoinCallResult = { Result.success(true) },
|
canUserJoinCallResult = { Result.success(true) },
|
||||||
typingNoticeResult = { Result.success(Unit) },
|
typingNoticeResult = { Result.success(Unit) },
|
||||||
canUserPinUnpinResult = { Result.success(true) },
|
canUserPinUnpinResult = { Result.success(true) },
|
||||||
)
|
)
|
||||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -1060,6 +1061,7 @@ class MessagesPresenterTest {
|
||||||
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
||||||
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||||
|
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
composerPresenter = messageComposerPresenter,
|
composerPresenter = messageComposerPresenter,
|
||||||
|
|
@ -1070,6 +1072,7 @@ class MessagesPresenterTest {
|
||||||
customReactionPresenter = customReactionPresenter,
|
customReactionPresenter = customReactionPresenter,
|
||||||
reactionSummaryPresenter = reactionSummaryPresenter,
|
reactionSummaryPresenter = reactionSummaryPresenter,
|
||||||
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
|
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
|
||||||
|
pinnedMessagesBannerPresenter = { aPinnedMessagesBannerState() },
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
navigator = navigator,
|
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
|
val SemanticColors.badgeNegativeContentColor
|
||||||
get() = if (isLight) LightColorTokens.colorRed1100 else DarkColorTokens.colorRed1100
|
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
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ColorAliasesPreview() = ElementPreview {
|
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