Skip to content

Commit 35928e3

Browse files
Threads - first iteration (#5165)
* Initial threads support: parse `ThreadSummary`. Replace several `isThreaded` values with `EventThreadInfo`, which contains the info about the event either being the root of a thread or part of it. * Add `Threaded` timeline mode * Add a `liveTimeline` parameter to `TimelineController`'s constructor. This way we can customise which timeline will be used as the 'live' one. Also add `@LiveTimeline` DI qualifier for the actual live timeline of the room. * Create `ThreadedMessagesNode`. Allow opening a thread in a separate screen. * Add the callbacks for the list menu actions - even if they're the wrong ones and will send the data to the room instead * Send attachments and location in threads * Fix polls in threads, add support for sending voice messages in threads * Display thread summaries only when the feature flag is enabled * Use 'Reply' instead of 'Reply in thread' when in threaded timeline mode * Remove incorrect usage of `Timeline` in `MessageComposerPresenter`. This led to replies to threaded events not appearing as actual replies. --------- Co-authored-by: ElementBot <android@element.io>
1 parent cc10ba4 commit 35928e3

File tree

119 files changed

+1520
-339
lines changed

Some content is hidden

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

119 files changed

+1520
-339
lines changed

features/location/api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ dependencies {
6565
implementation(projects.libraries.architecture)
6666
implementation(projects.libraries.designsystem)
6767
implementation(projects.libraries.core)
68+
implementation(projects.libraries.matrix.api)
6869
implementation(projects.libraries.matrixui)
6970
implementation(projects.libraries.uiStrings)
7071
implementation(libs.coil.compose)

features/location/api/src/main/kotlin/io/element/android/features/location/api/SendLocationEntryPoint.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@
77

88
package io.element.android.features.location.api
99

10-
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
10+
import com.bumble.appyx.core.modality.BuildContext
11+
import com.bumble.appyx.core.node.Node
12+
import io.element.android.libraries.architecture.FeatureEntryPoint
13+
import io.element.android.libraries.matrix.api.timeline.Timeline
1114

1215
/**
1316
* The "Send location" screen.
1417
*
1518
* Allows a user to share a location message within a room.
1619
*/
17-
interface SendLocationEntryPoint : SimpleFeatureEntryPoint
20+
interface SendLocationEntryPoint : FeatureEntryPoint {
21+
fun builder(timelineMode: Timeline.Mode): Builder
22+
interface Builder {
23+
fun build(parentNode: Node, buildContext: BuildContext): Node
24+
}
25+
}

features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/DefaultSendLocationEntryPoint.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@ import com.squareup.anvil.annotations.ContributesBinding
1313
import io.element.android.features.location.api.SendLocationEntryPoint
1414
import io.element.android.libraries.architecture.createNode
1515
import io.element.android.libraries.di.AppScope
16+
import io.element.android.libraries.matrix.api.timeline.Timeline
1617
import javax.inject.Inject
1718

1819
@ContributesBinding(AppScope::class)
1920
class DefaultSendLocationEntryPoint @Inject constructor() : SendLocationEntryPoint {
20-
override fun createNode(
21-
parentNode: Node,
22-
buildContext: BuildContext
23-
): SendLocationNode = parentNode.createNode(buildContext)
21+
override fun builder(timelineMode: Timeline.Mode): SendLocationEntryPoint.Builder {
22+
return Builder(timelineMode)
23+
}
24+
25+
class Builder(private val timelineMode: Timeline.Mode) : SendLocationEntryPoint.Builder {
26+
override fun build(parentNode: Node, buildContext: BuildContext): Node {
27+
return parentNode.createNode<SendLocationNode>(
28+
buildContext = buildContext,
29+
plugins = listOf(SendLocationNode.Inputs(timelineMode))
30+
)
31+
}
32+
}
2433
}

features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,25 @@ import dagger.assisted.Assisted
1717
import dagger.assisted.AssistedInject
1818
import im.vector.app.features.analytics.plan.MobileScreen
1919
import io.element.android.anvilannotations.ContributesNode
20+
import io.element.android.libraries.architecture.NodeInputs
21+
import io.element.android.libraries.architecture.inputs
2022
import io.element.android.libraries.di.RoomScope
23+
import io.element.android.libraries.matrix.api.timeline.Timeline
2124
import io.element.android.services.analytics.api.AnalyticsService
2225

2326
@ContributesNode(RoomScope::class)
2427
class SendLocationNode @AssistedInject constructor(
2528
@Assisted buildContext: BuildContext,
2629
@Assisted plugins: List<Plugin>,
27-
private val presenter: SendLocationPresenter,
30+
presenterFactory: SendLocationPresenter.Factory,
2831
analyticsService: AnalyticsService,
2932
) : Node(buildContext, plugins = plugins) {
33+
data class Inputs(
34+
val timelineMode: Timeline.Mode,
35+
) : NodeInputs
36+
37+
private val presenter = presenterFactory.create(inputs<Inputs>().timelineMode)
38+
3039
init {
3140
lifecycle.subscribe(
3241
onResume = {

features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import androidx.compose.runtime.mutableStateOf
1515
import androidx.compose.runtime.remember
1616
import androidx.compose.runtime.rememberCoroutineScope
1717
import androidx.compose.runtime.setValue
18+
import dagger.assisted.Assisted
19+
import dagger.assisted.AssistedFactory
20+
import dagger.assisted.AssistedInject
1821
import im.vector.app.features.analytics.plan.Composer
1922
import io.element.android.features.location.impl.common.MapDefaults
2023
import io.element.android.features.location.impl.common.actions.LocationActions
@@ -23,22 +26,30 @@ import io.element.android.features.location.impl.common.permissions.PermissionsP
2326
import io.element.android.features.location.impl.common.permissions.PermissionsState
2427
import io.element.android.features.messages.api.MessageComposerContext
2528
import io.element.android.libraries.architecture.Presenter
29+
import io.element.android.libraries.core.extensions.flatMap
2630
import io.element.android.libraries.core.meta.BuildMeta
31+
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
2732
import io.element.android.libraries.matrix.api.room.JoinedRoom
2833
import io.element.android.libraries.matrix.api.room.location.AssetType
34+
import io.element.android.libraries.matrix.api.timeline.Timeline
2935
import io.element.android.libraries.textcomposer.model.MessageComposerMode
3036
import io.element.android.services.analytics.api.AnalyticsService
3137
import kotlinx.coroutines.launch
32-
import javax.inject.Inject
3338

34-
class SendLocationPresenter @Inject constructor(
39+
class SendLocationPresenter @AssistedInject constructor(
3540
permissionsPresenterFactory: PermissionsPresenter.Factory,
3641
private val room: JoinedRoom,
42+
@Assisted private val timelineMode: Timeline.Mode,
3743
private val analyticsService: AnalyticsService,
3844
private val messageComposerContext: MessageComposerContext,
3945
private val locationActions: LocationActions,
4046
private val buildMeta: BuildMeta,
4147
) : Presenter<SendLocationState> {
48+
@AssistedFactory
49+
interface Factory {
50+
fun create(timelineMode: Timeline.Mode): SendLocationPresenter
51+
}
52+
4253
private val permissionsPresenter = permissionsPresenterFactory.create(MapDefaults.permissions)
4354

4455
@Composable
@@ -104,14 +115,16 @@ class SendLocationPresenter @Inject constructor(
104115
when (mode) {
105116
SendLocationState.Mode.PinLocation -> {
106117
val geoUri = event.cameraPosition.toGeoUri()
107-
room.liveTimeline.sendLocation(
108-
body = generateBody(geoUri),
109-
geoUri = geoUri,
110-
description = null,
111-
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
112-
assetType = AssetType.PIN,
113-
inReplyToEventId = inReplyToEventId,
114-
)
118+
getTimeline().flatMap {
119+
it.sendLocation(
120+
body = generateBody(geoUri),
121+
geoUri = geoUri,
122+
description = null,
123+
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
124+
assetType = AssetType.PIN,
125+
inReplyToEventId = inReplyToEventId,
126+
)
127+
}
115128
analyticsService.capture(
116129
Composer(
117130
inThread = messageComposerContext.composerMode.inThread,
@@ -123,14 +136,16 @@ class SendLocationPresenter @Inject constructor(
123136
}
124137
SendLocationState.Mode.SenderLocation -> {
125138
val geoUri = event.toGeoUri()
126-
room.liveTimeline.sendLocation(
127-
body = generateBody(geoUri),
128-
geoUri = geoUri,
129-
description = null,
130-
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
131-
assetType = AssetType.SENDER,
132-
inReplyToEventId = inReplyToEventId,
133-
)
139+
getTimeline().flatMap {
140+
it.sendLocation(
141+
body = generateBody(geoUri),
142+
geoUri = geoUri,
143+
description = null,
144+
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
145+
assetType = AssetType.SENDER,
146+
inReplyToEventId = inReplyToEventId,
147+
)
148+
}
134149
analyticsService.capture(
135150
Composer(
136151
inThread = messageComposerContext.composerMode.inThread,
@@ -142,6 +157,13 @@ class SendLocationPresenter @Inject constructor(
142157
}
143158
}
144159
}
160+
161+
private suspend fun getTimeline(): Result<Timeline> {
162+
return when (timelineMode) {
163+
is Timeline.Mode.Thread -> room.createTimeline(CreateTimelineParams.Threaded(timelineMode.threadRootId))
164+
else -> Result.success(room.liveTimeline)
165+
}
166+
}
145167
}
146168

147169
private fun SendLocationEvents.SendLocation.toGeoUri(): String = location?.toGeoUri() ?: cameraPosition.toGeoUri()

features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import io.element.android.features.messages.test.FakeMessageComposerContext
2323
import io.element.android.libraries.matrix.api.core.EventId
2424
import io.element.android.libraries.matrix.api.room.JoinedRoom
2525
import io.element.android.libraries.matrix.api.room.location.AssetType
26+
import io.element.android.libraries.matrix.api.timeline.Timeline
2627
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
2728
import io.element.android.libraries.matrix.test.AN_EVENT_ID
2829
import io.element.android.libraries.matrix.test.core.aBuildMeta
@@ -55,6 +56,7 @@ class SendLocationPresenterTest {
5556
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
5657
},
5758
room = joinedRoom,
59+
timelineMode = Timeline.Mode.Live,
5860
analyticsService = fakeAnalyticsService,
5961
messageComposerContext = fakeMessageComposerContext,
6062
locationActions = fakeLocationActions,

features/messages/api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ android {
1616

1717
dependencies {
1818
implementation(projects.libraries.architecture)
19+
implementation(projects.libraries.designsystem)
1920
implementation(projects.libraries.matrix.api)
2021
implementation(projects.libraries.mediaviewer.api)
2122
implementation(projects.libraries.preferences.api)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
package io.element.android.features.messages.impl.voicemessages.composer
8+
package io.element.android.features.messages.api.timeline.voicemessages.composer
99

1010
import androidx.lifecycle.Lifecycle
1111
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.messages.api.timeline.voicemessages.composer
9+
10+
import io.element.android.libraries.architecture.Presenter
11+
import io.element.android.libraries.matrix.api.timeline.Timeline
12+
13+
fun interface VoiceMessageComposerPresenter : Presenter<VoiceMessageComposerState> {
14+
interface Factory {
15+
fun create(timelineMode: Timeline.Mode): VoiceMessageComposerPresenter
16+
}
17+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Please see LICENSE files in the repository root for full details.
66
*/
77

8-
package io.element.android.features.messages.impl.voicemessages.composer
8+
package io.element.android.features.messages.api.timeline.voicemessages.composer
99

1010
import androidx.compose.runtime.Stable
1111
import io.element.android.libraries.textcomposer.model.VoiceMessageState

0 commit comments

Comments
 (0)