Skip to content

Commit 0bcfffd

Browse files
authored
Merge pull request #3259 from element-hq/feature/fga/pinned_message_banner_ui
WIP Pinned events : start creating the banner ui, no logic.
2 parents 6b8dba6 + 6ee86a2 commit 0bcfffd

File tree

29 files changed

+591
-34
lines changed

29 files changed

+591
-34
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
4141
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
4242
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
4343
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
44+
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
4445
import io.element.android.features.messages.impl.timeline.TimelineController
4546
import io.element.android.features.messages.impl.timeline.TimelineEvents
4647
import io.element.android.features.messages.impl.timeline.TimelinePresenter
@@ -101,6 +102,7 @@ class MessagesPresenter @AssistedInject constructor(
101102
private val customReactionPresenter: CustomReactionPresenter,
102103
private val reactionSummaryPresenter: ReactionSummaryPresenter,
103104
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
105+
private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>,
104106
private val networkMonitor: NetworkMonitor,
105107
private val snackbarDispatcher: SnackbarDispatcher,
106108
private val dispatchers: CoroutineDispatchers,
@@ -132,6 +134,7 @@ class MessagesPresenter @AssistedInject constructor(
132134
val customReactionState = customReactionPresenter.present()
133135
val reactionSummaryState = reactionSummaryPresenter.present()
134136
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
137+
val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present()
135138

136139
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
137140

@@ -230,6 +233,7 @@ class MessagesPresenter @AssistedInject constructor(
230233
enableVoiceMessages = enableVoiceMessages,
231234
appName = buildMeta.applicationName,
232235
callState = callState,
236+
pinnedMessagesBannerState = pinnedMessagesBannerState,
233237
eventSink = { handleEvents(it) }
234238
)
235239
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package io.element.android.features.messages.impl
1919
import androidx.compose.runtime.Immutable
2020
import io.element.android.features.messages.impl.actionlist.ActionListState
2121
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
22+
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
2223
import io.element.android.features.messages.impl.timeline.TimelineState
2324
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
2425
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
@@ -54,6 +55,7 @@ data class MessagesState(
5455
val enableVoiceMessages: Boolean,
5556
val callState: RoomCallState,
5657
val appName: String,
58+
val pinnedMessagesBannerState: PinnedMessagesBannerState,
5759
val eventSink: (MessagesEvents) -> Unit
5860
)
5961

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import io.element.android.features.messages.impl.actionlist.anActionListState
2222
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
2323
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
2424
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
25+
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
26+
import io.element.android.features.messages.impl.pinned.banner.aPinnedMessagesBannerState
2527
import io.element.android.features.messages.impl.timeline.TimelineState
2628
import io.element.android.features.messages.impl.timeline.aTimelineItemList
2729
import io.element.android.features.messages.impl.timeline.aTimelineState
@@ -87,6 +89,12 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
8789
aMessagesState(
8890
callState = RoomCallState.DISABLED,
8991
),
92+
aMessagesState(
93+
pinnedMessagesBannerState = aPinnedMessagesBannerState(
94+
pinnedMessagesCount = 4,
95+
currentPinnedMessageIndex = 0,
96+
),
97+
),
9098
)
9199
}
92100

@@ -113,6 +121,7 @@ fun aMessagesState(
113121
showReinvitePrompt: Boolean = false,
114122
enableVoiceMessages: Boolean = true,
115123
callState: RoomCallState = RoomCallState.ENABLED,
124+
pinnedMessagesBannerState: PinnedMessagesBannerState = aPinnedMessagesBannerState(),
116125
eventSink: (MessagesEvents) -> Unit = {},
117126
) = MessagesState(
118127
roomId = RoomId("!id:domain"),
@@ -136,6 +145,7 @@ fun aMessagesState(
136145
enableVoiceMessages = enableVoiceMessages,
137146
callState = callState,
138147
appName = "Element",
148+
pinnedMessagesBannerState = pinnedMessagesBannerState,
139149
eventSink = eventSink,
140150
)
141151

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package io.element.android.features.messages.impl
1818

19+
import androidx.compose.animation.AnimatedVisibility
20+
import androidx.compose.animation.expandVertically
21+
import androidx.compose.animation.shrinkVertically
1922
import androidx.compose.foundation.background
2023
import androidx.compose.foundation.clickable
2124
import androidx.compose.foundation.layout.Arrangement
@@ -33,6 +36,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
3336
import androidx.compose.foundation.layout.padding
3437
import androidx.compose.foundation.layout.statusBars
3538
import androidx.compose.foundation.layout.width
39+
import androidx.compose.foundation.lazy.rememberLazyListState
3640
import androidx.compose.foundation.shape.RoundedCornerShape
3741
import androidx.compose.material3.ExperimentalMaterial3Api
3842
import androidx.compose.material3.MaterialTheme
@@ -68,6 +72,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsBott
6872
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
6973
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
7074
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
75+
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView
7176
import io.element.android.features.messages.impl.timeline.TimelineView
7277
import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem
7378
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
@@ -100,6 +105,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
100105
import io.element.android.libraries.designsystem.theme.components.TopAppBar
101106
import io.element.android.libraries.designsystem.utils.KeepScreenOn
102107
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
108+
import io.element.android.libraries.designsystem.utils.isScrollingUp
103109
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
104110
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
105111
import io.element.android.libraries.matrix.api.core.UserId
@@ -370,22 +376,34 @@ private fun MessagesViewContent(
370376
RectangleShape
371377
},
372378
content = { paddingValues ->
373-
TimelineView(
374-
state = state.timelineState,
375-
typingNotificationState = state.typingNotificationState,
376-
onUserDataClick = onUserDataClick,
377-
onLinkClick = onLinkClick,
378-
onMessageClick = onMessageClick,
379-
onMessageLongClick = onMessageLongClick,
380-
onSwipeToReply = onSwipeToReply,
381-
onReactionClick = onReactionClick,
382-
onReactionLongClick = onReactionLongClick,
383-
onMoreReactionsClick = onMoreReactionsClick,
384-
onReadReceiptClick = onReadReceiptClick,
385-
modifier = Modifier.padding(paddingValues),
386-
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
387-
onJoinCallClick = onJoinCallClick,
388-
)
379+
Box(modifier = Modifier.padding(paddingValues)) {
380+
val timelineLazyListState = rememberLazyListState()
381+
TimelineView(
382+
state = state.timelineState,
383+
typingNotificationState = state.typingNotificationState,
384+
onUserDataClick = onUserDataClick,
385+
onLinkClick = onLinkClick,
386+
onMessageClick = onMessageClick,
387+
onMessageLongClick = onMessageLongClick,
388+
onSwipeToReply = onSwipeToReply,
389+
onReactionClick = onReactionClick,
390+
onReactionLongClick = onReactionLongClick,
391+
onMoreReactionsClick = onMoreReactionsClick,
392+
onReadReceiptClick = onReadReceiptClick,
393+
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
394+
onJoinCallClick = onJoinCallClick,
395+
lazyListState = timelineLazyListState,
396+
)
397+
AnimatedVisibility(
398+
visible = state.pinnedMessagesBannerState.displayBanner && timelineLazyListState.isScrollingUp(),
399+
enter = expandVertically(),
400+
exit = shrinkVertically(),
401+
) {
402+
PinnedMessagesBannerView(
403+
state = state.pinnedMessagesBannerState,
404+
)
405+
}
406+
}
389407
},
390408
sheetContent = { subcomposing: Boolean ->
391409
MessagesViewComposerBottomSheetContents(
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.impl.di
18+
19+
import com.squareup.anvil.annotations.ContributesTo
20+
import dagger.Binds
21+
import dagger.Module
22+
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
23+
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
24+
import io.element.android.libraries.architecture.Presenter
25+
import io.element.android.libraries.di.SessionScope
26+
27+
@ContributesTo(SessionScope::class)
28+
@Module
29+
interface MessagesModule {
30+
@Binds
31+
fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter<PinnedMessagesBannerState>
32+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.impl.pinned.banner
18+
19+
sealed interface PinnedMessagesBannerEvents {
20+
data object MoveToNextPinned : PinnedMessagesBannerEvents
21+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.impl.pinned.banner
18+
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.getValue
21+
import androidx.compose.runtime.mutableIntStateOf
22+
import androidx.compose.runtime.remember
23+
import androidx.compose.runtime.saveable.rememberSaveable
24+
import androidx.compose.runtime.setValue
25+
import io.element.android.libraries.architecture.Presenter
26+
import javax.inject.Inject
27+
28+
class PinnedMessagesBannerPresenter @Inject constructor() : Presenter<PinnedMessagesBannerState> {
29+
@Composable
30+
override fun present(): PinnedMessagesBannerState {
31+
var pinnedMessageCount by remember {
32+
mutableIntStateOf(0)
33+
}
34+
var currentPinnedMessageIndex by rememberSaveable {
35+
mutableIntStateOf(0)
36+
}
37+
38+
fun handleEvent(event: PinnedMessagesBannerEvents) {
39+
when (event) {
40+
is PinnedMessagesBannerEvents.MoveToNextPinned -> {
41+
if (currentPinnedMessageIndex < pinnedMessageCount - 1) {
42+
currentPinnedMessageIndex++
43+
} else {
44+
currentPinnedMessageIndex = 0
45+
}
46+
}
47+
}
48+
}
49+
50+
return PinnedMessagesBannerState(
51+
pinnedMessagesCount = pinnedMessageCount,
52+
currentPinnedMessageIndex = currentPinnedMessageIndex,
53+
eventSink = ::handleEvent
54+
)
55+
}
56+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.impl.pinned.banner
18+
19+
data class PinnedMessagesBannerState(
20+
val pinnedMessagesCount: Int,
21+
val currentPinnedMessageIndex: Int,
22+
val eventSink: (PinnedMessagesBannerEvents) -> Unit
23+
) {
24+
val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessageIndex < pinnedMessagesCount
25+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.impl.pinned.banner
18+
19+
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
20+
21+
internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider<PinnedMessagesBannerState> {
22+
override val values: Sequence<PinnedMessagesBannerState>
23+
get() = sequenceOf(
24+
aPinnedMessagesBannerState(pinnedMessagesCount = 1, currentPinnedMessageIndex = 0),
25+
aPinnedMessagesBannerState(pinnedMessagesCount = 2, currentPinnedMessageIndex = 0),
26+
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 0),
27+
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 1),
28+
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 2),
29+
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 3),
30+
)
31+
}
32+
33+
internal fun aPinnedMessagesBannerState(
34+
pinnedMessagesCount: Int = 0,
35+
currentPinnedMessageIndex: Int = -1,
36+
eventSink: (PinnedMessagesBannerEvents) -> Unit = {}
37+
) = PinnedMessagesBannerState(
38+
pinnedMessagesCount = pinnedMessagesCount,
39+
currentPinnedMessageIndex = currentPinnedMessageIndex,
40+
eventSink = eventSink
41+
)

0 commit comments

Comments
 (0)