Skip to content

Commit 9cdec78

Browse files
authored
Merge pull request #3275 from element-hq/feature/fga/pinned_message_banner_logic
[Feature] Pinned message : banner logic
2 parents e6ba45d + a5d633a commit 9cdec78

File tree

83 files changed

+1175
-240
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+1175
-240
lines changed

features/messages/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,6 @@ dependencies {
102102
testImplementation(projects.features.poll.test)
103103
testImplementation(projects.features.poll.impl)
104104
testImplementation(libs.androidx.compose.ui.test.junit)
105+
testImplementation(projects.libraries.eventformatter.test)
105106
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
106107
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import kotlinx.collections.immutable.ImmutableList
8181
import kotlinx.coroutines.flow.launchIn
8282
import kotlinx.coroutines.flow.onEach
8383
import kotlinx.parcelize.Parcelize
84+
import timber.log.Timber
8485

8586
@ContributesNode(RoomScope::class)
8687
class MessagesFlowNode @AssistedInject constructor(
@@ -217,6 +218,10 @@ class MessagesFlowNode @AssistedInject constructor(
217218
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
218219
elementCallEntryPoint.startCall(callType)
219220
}
221+
222+
override fun onViewAllPinnedEvents() {
223+
Timber.d("On View All Pinned Events not implemented yet.")
224+
}
220225
}
221226
val inputs = MessagesNode.Inputs(
222227
focusedEventId = inputs.focusedEventId,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class MessagesNode @AssistedInject constructor(
9797
fun onCreatePollClick()
9898
fun onEditPollClick(eventId: EventId)
9999
fun onJoinCallClick(roomId: RoomId)
100+
fun onViewAllPinnedEvents()
100101
}
101102

102103
override fun onBuilt() {
@@ -185,6 +186,10 @@ class MessagesNode @AssistedInject constructor(
185186
callbacks.forEach { it.onEditPollClick(eventId) }
186187
}
187188

189+
private fun onViewAllPinnedMessagesClick() {
190+
callbacks.forEach { it.onViewAllPinnedEvents() }
191+
}
192+
188193
private fun onSendLocationClick() {
189194
callbacks.forEach { it.onSendLocationClick() }
190195
}
@@ -221,6 +226,7 @@ class MessagesNode @AssistedInject constructor(
221226
onSendLocationClick = this::onSendLocationClick,
222227
onCreatePollClick = this::onCreatePollClick,
223228
onJoinCallClick = this::onJoinCallClick,
229+
onViewAllPinnedMessagesClick = this::onViewAllPinnedMessagesClick,
224230
modifier = modifier,
225231
)
226232

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat
2323
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
2424
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
2525
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
26-
import io.element.android.features.messages.impl.pinned.banner.aPinnedMessagesBannerState
26+
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
2727
import io.element.android.features.messages.impl.timeline.TimelineState
2828
import io.element.android.features.messages.impl.timeline.aTimelineItemList
2929
import io.element.android.features.messages.impl.timeline.aTimelineState
@@ -90,8 +90,8 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
9090
callState = RoomCallState.DISABLED,
9191
),
9292
aMessagesState(
93-
pinnedMessagesBannerState = aPinnedMessagesBannerState(
94-
pinnedMessagesCount = 4,
93+
pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(
94+
knownPinnedMessagesCount = 4,
9595
currentPinnedMessageIndex = 0,
9696
),
9797
),
@@ -121,7 +121,7 @@ fun aMessagesState(
121121
showReinvitePrompt: Boolean = false,
122122
enableVoiceMessages: Boolean = true,
123123
callState: RoomCallState = RoomCallState.ENABLED,
124-
pinnedMessagesBannerState: PinnedMessagesBannerState = aPinnedMessagesBannerState(),
124+
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
125125
eventSink: (MessagesEvents) -> Unit = {},
126126
) = MessagesState(
127127
roomId = RoomId("!id:domain"),

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding
3636
import androidx.compose.foundation.layout.padding
3737
import androidx.compose.foundation.layout.statusBars
3838
import androidx.compose.foundation.layout.width
39-
import androidx.compose.foundation.lazy.rememberLazyListState
4039
import androidx.compose.foundation.shape.RoundedCornerShape
4140
import androidx.compose.material3.ExperimentalMaterial3Api
4241
import androidx.compose.material3.MaterialTheme
@@ -72,7 +71,11 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsBott
7271
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
7372
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
7473
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
74+
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
7575
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView
76+
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerViewDefaults
77+
import io.element.android.features.messages.impl.timeline.FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS
78+
import io.element.android.features.messages.impl.timeline.TimelineEvents
7679
import io.element.android.features.messages.impl.timeline.TimelineView
7780
import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem
7881
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
@@ -105,14 +108,15 @@ import io.element.android.libraries.designsystem.theme.components.Text
105108
import io.element.android.libraries.designsystem.theme.components.TopAppBar
106109
import io.element.android.libraries.designsystem.utils.KeepScreenOn
107110
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
108-
import io.element.android.libraries.designsystem.utils.isScrollingUp
109111
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
110112
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
113+
import io.element.android.libraries.matrix.api.core.EventId
111114
import io.element.android.libraries.matrix.api.core.UserId
112115
import io.element.android.libraries.ui.strings.CommonStrings
113116
import kotlinx.collections.immutable.ImmutableList
114117
import timber.log.Timber
115118
import kotlin.random.Random
119+
import kotlin.time.Duration.Companion.milliseconds
116120

117121
@Composable
118122
fun MessagesView(
@@ -126,8 +130,9 @@ fun MessagesView(
126130
onSendLocationClick: () -> Unit,
127131
onCreatePollClick: () -> Unit,
128132
onJoinCallClick: () -> Unit,
133+
onViewAllPinnedMessagesClick: () -> Unit,
129134
modifier: Modifier = Modifier,
130-
forceJumpToBottomVisibility: Boolean = false
135+
forceJumpToBottomVisibility: Boolean = false,
131136
) {
132137
OnLifecycleEvent { _, event ->
133138
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event))
@@ -228,6 +233,7 @@ fun MessagesView(
228233
},
229234
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
230235
onJoinCallClick = onJoinCallClick,
236+
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
231237
)
232238
},
233239
snackbarHost = {
@@ -319,6 +325,7 @@ private fun MessagesViewContent(
319325
onSendLocationClick: () -> Unit,
320326
onCreatePollClick: () -> Unit,
321327
onJoinCallClick: () -> Unit,
328+
onViewAllPinnedMessagesClick: () -> Unit,
322329
forceJumpToBottomVisibility: Boolean,
323330
modifier: Modifier = Modifier,
324331
onSwipeToReply: (TimelineItem.Event) -> Unit,
@@ -377,7 +384,7 @@ private fun MessagesViewContent(
377384
},
378385
content = { paddingValues ->
379386
Box(modifier = Modifier.padding(paddingValues)) {
380-
val timelineLazyListState = rememberLazyListState()
387+
val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberExitOnScrollBehavior()
381388
TimelineView(
382389
state = state.timelineState,
383390
typingNotificationState = state.typingNotificationState,
@@ -392,15 +399,22 @@ private fun MessagesViewContent(
392399
onReadReceiptClick = onReadReceiptClick,
393400
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
394401
onJoinCallClick = onJoinCallClick,
395-
lazyListState = timelineLazyListState,
402+
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
396403
)
397404
AnimatedVisibility(
398-
visible = state.pinnedMessagesBannerState.displayBanner && timelineLazyListState.isScrollingUp(),
405+
visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible,
399406
enter = expandVertically(),
400407
exit = shrinkVertically(),
401408
) {
409+
fun focusOnPinnedEvent(eventId: EventId) {
410+
state.timelineState.eventSink(
411+
TimelineEvents.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)
412+
)
413+
}
402414
PinnedMessagesBannerView(
403415
state = state.pinnedMessagesBannerState,
416+
onClick = ::focusOnPinnedEvent,
417+
onViewAllClick = onViewAllPinnedMessagesClick,
404418
)
405419
}
406420
}
@@ -572,12 +586,13 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
572586
onBackClick = {},
573587
onRoomDetailsClick = {},
574588
onEventClick = { false },
575-
onPreviewAttachments = {},
576589
onUserDataClick = {},
577590
onLinkClick = {},
591+
onPreviewAttachments = {},
578592
onSendLocationClick = {},
579593
onCreatePollClick = {},
580594
onJoinCallClick = {},
595+
onViewAllPinnedMessagesClick = { },
581596
forceJumpToBottomVisibility = true,
582597
)
583598
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ import dagger.Module
2222
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
2323
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
2424
import io.element.android.libraries.architecture.Presenter
25-
import io.element.android.libraries.di.SessionScope
25+
import io.element.android.libraries.di.RoomScope
2626

27-
@ContributesTo(SessionScope::class)
27+
@ContributesTo(RoomScope::class)
2828
@Module
2929
interface MessagesModule {
3030
@Binds
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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
18+
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.LaunchedEffect
21+
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableStateOf
23+
import androidx.compose.runtime.saveable.rememberSaveable
24+
import androidx.compose.runtime.setValue
25+
import com.squareup.anvil.annotations.ContributesBinding
26+
import io.element.android.libraries.di.AppScope
27+
import io.element.android.libraries.featureflag.api.FeatureFlagService
28+
import io.element.android.libraries.featureflag.api.FeatureFlags
29+
import kotlinx.coroutines.flow.launchIn
30+
import kotlinx.coroutines.flow.onEach
31+
import javax.inject.Inject
32+
33+
fun interface IsPinnedMessagesFeatureEnabled {
34+
@Composable
35+
operator fun invoke(): Boolean
36+
}
37+
38+
@ContributesBinding(AppScope::class)
39+
class DefaultIsPinnedMessagesFeatureEnabled @Inject constructor(
40+
private val featureFlagService: FeatureFlagService,
41+
) : IsPinnedMessagesFeatureEnabled {
42+
@Composable
43+
override operator fun invoke(): Boolean {
44+
var isFeatureEnabled by rememberSaveable {
45+
mutableStateOf(false)
46+
}
47+
LaunchedEffect(Unit) {
48+
featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents)
49+
.onEach { isFeatureEnabled = it }
50+
.launchIn(this)
51+
}
52+
return isFeatureEnabled
53+
}
54+
}
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+
import androidx.compose.ui.text.AnnotatedString
20+
import io.element.android.libraries.matrix.api.core.EventId
21+
22+
data class PinnedMessagesBannerItem(
23+
val eventId: EventId,
24+
val formatted: AnnotatedString,
25+
)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.text.AnnotatedString
20+
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
21+
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter
22+
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
23+
import kotlinx.coroutines.withContext
24+
import javax.inject.Inject
25+
26+
class PinnedMessagesBannerItemFactory @Inject constructor(
27+
private val coroutineDispatchers: CoroutineDispatchers,
28+
private val formatter: PinnedMessagesBannerFormatter,
29+
) {
30+
suspend fun create(timelineItem: MatrixTimelineItem): PinnedMessagesBannerItem? = withContext(coroutineDispatchers.computation) {
31+
when (timelineItem) {
32+
is MatrixTimelineItem.Event -> {
33+
val eventId = timelineItem.eventId ?: return@withContext null
34+
val formatted = formatter.format(timelineItem.event)
35+
PinnedMessagesBannerItem(
36+
eventId = eventId,
37+
formatted = if (formatted is AnnotatedString) {
38+
formatted
39+
} else {
40+
AnnotatedString(formatted.toString())
41+
},
42+
)
43+
}
44+
else -> null
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)