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>
This commit is contained in:
Jorge Martin Espinosa 2025-08-19 15:35:48 +02:00 committed by GitHub
parent 8675bf5215
commit 285066c206
119 changed files with 1520 additions and 339 deletions

View file

@ -16,6 +16,7 @@ android {
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.mediaviewer.api)
implementation(projects.libraries.preferences.api)

View file

@ -0,0 +1,27 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.messages.api.timeline.voicemessages.composer
import androidx.lifecycle.Lifecycle
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
sealed interface VoiceMessageComposerEvents {
data class RecorderEvent(
val recorderEvent: VoiceMessageRecorderEvent
) : VoiceMessageComposerEvents
data class PlayerEvent(
val playerEvent: VoiceMessagePlayerEvent,
) : VoiceMessageComposerEvents
data object SendVoiceMessage : VoiceMessageComposerEvents
data object DeleteVoiceMessage : VoiceMessageComposerEvents
data object AcceptPermissionRationale : VoiceMessageComposerEvents
data object DismissPermissionsRationale : VoiceMessageComposerEvents
data class LifecycleEvent(val event: Lifecycle.Event) : VoiceMessageComposerEvents
data object DismissSendFailureDialog : VoiceMessageComposerEvents
}

View file

@ -0,0 +1,17 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.messages.api.timeline.voicemessages.composer
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.timeline.Timeline
fun interface VoiceMessageComposerPresenter : Presenter<VoiceMessageComposerState> {
interface Factory {
fun create(timelineMode: Timeline.Mode): VoiceMessageComposerPresenter
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.messages.api.timeline.voicemessages.composer
import androidx.compose.runtime.Stable
import io.element.android.libraries.textcomposer.model.VoiceMessageState
@Stable
data class VoiceMessageComposerState(
val voiceMessageState: VoiceMessageState,
val showPermissionRationaleDialog: Boolean,
val showSendFailureDialog: Boolean,
val keepScreenOn: Boolean,
val eventSink: (VoiceMessageComposerEvents) -> Unit,
)

View file

@ -0,0 +1,45 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.messages.api.timeline.voicemessages.composer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.media.createFakeWaveform
import io.element.android.libraries.textcomposer.model.VoiceMessageState
import kotlinx.collections.immutable.toPersistentList
import kotlin.time.Duration.Companion.seconds
open class VoiceMessageComposerStateProvider : PreviewParameterProvider<VoiceMessageComposerState> {
override val values: Sequence<VoiceMessageComposerState>
get() = sequenceOf(
aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, levels = aWaveformLevels)),
)
}
fun aVoiceMessageComposerState(
voiceMessageState: VoiceMessageState = VoiceMessageState.Idle,
keepScreenOn: Boolean = false,
showPermissionRationaleDialog: Boolean = false,
showSendFailureDialog: Boolean = false,
) = VoiceMessageComposerState(
voiceMessageState = voiceMessageState,
showPermissionRationaleDialog = showPermissionRationaleDialog,
showSendFailureDialog = showSendFailureDialog,
keepScreenOn = keepScreenOn,
eventSink = {},
)
fun aVoiceMessagePreviewState() = VoiceMessageState.Preview(
isSending = false,
isPlaying = false,
showCursor = false,
playbackProgress = 0f,
time = 10.seconds,
waveform = createFakeWaveform(),
)
internal var aWaveformLevels = List(100) { it.toFloat() / 100 }.toPersistentList()