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:
committed by
GitHub
parent
8675bf5215
commit
285066c206
@@ -8,7 +8,8 @@
|
||||
package io.element.android.features.poll.api.actions
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
||||
interface EndPollAction {
|
||||
suspend fun execute(pollStartId: EventId): Result<Unit>
|
||||
suspend fun execute(timeline: Timeline, pollStartId: EventId): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
package io.element.android.features.poll.api.actions
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
||||
interface SendPollResponseAction {
|
||||
suspend fun execute(pollStartId: EventId, answerId: String): Result<Unit>
|
||||
suspend fun execute(
|
||||
timeline: Timeline,
|
||||
pollStartId: EventId,
|
||||
answerId: String
|
||||
): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@ package io.element.android.features.poll.api.create
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
||||
interface CreatePollEntryPoint : FeatureEntryPoint {
|
||||
data class Params(
|
||||
val timelineMode: Timeline.Mode,
|
||||
val mode: CreatePollMode,
|
||||
)
|
||||
|
||||
|
||||
@@ -12,17 +12,16 @@ import im.vector.app.features.analytics.plan.PollEnd
|
||||
import io.element.android.features.poll.api.actions.EndPollAction
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultEndPollAction @Inject constructor(
|
||||
private val room: JoinedRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : EndPollAction {
|
||||
override suspend fun execute(pollStartId: EventId): Result<Unit> {
|
||||
return room.liveTimeline.endPoll(
|
||||
override suspend fun execute(timeline: Timeline, pollStartId: EventId): Result<Unit> {
|
||||
return timeline.endPoll(
|
||||
pollStartId = pollStartId,
|
||||
text = "The poll with event id: $pollStartId has ended."
|
||||
).onSuccess {
|
||||
|
||||
@@ -12,17 +12,16 @@ import im.vector.app.features.analytics.plan.PollVote
|
||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultSendPollResponseAction @Inject constructor(
|
||||
private val room: JoinedRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : SendPollResponseAction {
|
||||
override suspend fun execute(pollStartId: EventId, answerId: String): Result<Unit> {
|
||||
return room.liveTimeline.sendPollResponse(
|
||||
override suspend fun execute(timeline: Timeline, pollStartId: EventId, answerId: String): Result<Unit> {
|
||||
return timeline.sendPollResponse(
|
||||
pollStartId = pollStartId,
|
||||
answers = listOf(answerId),
|
||||
).onSuccess {
|
||||
|
||||
@@ -21,6 +21,7 @@ import io.element.android.features.poll.api.create.CreatePollMode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@@ -31,7 +32,7 @@ class CreatePollNode @AssistedInject constructor(
|
||||
presenterFactory: CreatePollPresenter.Factory,
|
||||
analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
data class Inputs(val mode: CreatePollMode) : NodeInputs
|
||||
data class Inputs(val mode: CreatePollMode, val timelineMode: Timeline.Mode) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
||||
@@ -44,6 +45,7 @@ class CreatePollNode @AssistedInject constructor(
|
||||
}
|
||||
},
|
||||
mode = inputs.mode,
|
||||
timelineMode = inputs.timelineMode,
|
||||
)
|
||||
|
||||
init {
|
||||
|
||||
@@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.poll.isDisclosed
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -37,17 +38,24 @@ import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class CreatePollPresenter @AssistedInject constructor(
|
||||
private val repository: PollRepository,
|
||||
repositoryFactory: PollRepository.Factory,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val messageComposerContext: MessageComposerContext,
|
||||
@Assisted private val navigateUp: () -> Unit,
|
||||
@Assisted private val mode: CreatePollMode,
|
||||
@Assisted private val timelineMode: Timeline.Mode,
|
||||
) : Presenter<CreatePollState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(backNavigator: () -> Unit, mode: CreatePollMode): CreatePollPresenter
|
||||
fun create(
|
||||
timelineMode: Timeline.Mode,
|
||||
backNavigator: () -> Unit,
|
||||
mode: CreatePollMode
|
||||
): CreatePollPresenter
|
||||
}
|
||||
|
||||
private val repository = repositoryFactory.create(timelineMode)
|
||||
|
||||
@Composable
|
||||
override fun present(): CreatePollState {
|
||||
// The initial state of the form. In edit mode this will be populated with the poll being edited.
|
||||
|
||||
@@ -23,7 +23,7 @@ class DefaultCreatePollEntryPoint @Inject constructor() : CreatePollEntryPoint {
|
||||
|
||||
return object : CreatePollEntryPoint.NodeBuilder {
|
||||
override fun params(params: CreatePollEntryPoint.Params): CreatePollEntryPoint.NodeBuilder {
|
||||
plugins += CreatePollNode.Inputs(mode = params.mode)
|
||||
plugins += CreatePollNode.Inputs(timelineMode = params.timelineMode, mode = params.mode)
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
@@ -7,24 +7,40 @@
|
||||
|
||||
package io.element.android.features.poll.impl.data
|
||||
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
class PollRepository @Inject constructor(
|
||||
class PollRepository @AssistedInject constructor(
|
||||
private val room: JoinedRoom,
|
||||
private val timelineProvider: TimelineProvider,
|
||||
private val defaultTimelineProvider: TimelineProvider,
|
||||
@Assisted private val timelineMode: Timeline.Mode,
|
||||
) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
timelineMode: Timeline.Mode,
|
||||
): PollRepository
|
||||
}
|
||||
|
||||
suspend fun getPoll(eventId: EventId): Result<PollContent> = runCatchingExceptions {
|
||||
timelineProvider
|
||||
getTimelineProvider()
|
||||
.getOrThrow()
|
||||
.getActiveTimeline()
|
||||
.timelineItems
|
||||
.first()
|
||||
@@ -42,30 +58,51 @@ class PollRepository @Inject constructor(
|
||||
pollKind: PollKind,
|
||||
maxSelections: Int,
|
||||
): Result<Unit> = when (existingPollId) {
|
||||
null -> room.liveTimeline.createPoll(
|
||||
question = question,
|
||||
answers = answers,
|
||||
maxSelections = maxSelections,
|
||||
pollKind = pollKind,
|
||||
)
|
||||
else -> timelineProvider
|
||||
.getActiveTimeline()
|
||||
.editPoll(
|
||||
pollStartId = existingPollId,
|
||||
question = question,
|
||||
answers = answers,
|
||||
maxSelections = maxSelections,
|
||||
pollKind = pollKind,
|
||||
)
|
||||
null -> getTimelineProvider().flatMap { timelineProvider ->
|
||||
timelineProvider
|
||||
.getActiveTimeline()
|
||||
.createPoll(
|
||||
question = question,
|
||||
answers = answers,
|
||||
maxSelections = maxSelections,
|
||||
pollKind = pollKind,
|
||||
)
|
||||
}
|
||||
else -> getTimelineProvider().flatMap { timelineProvider ->
|
||||
timelineProvider.getActiveTimeline()
|
||||
.editPoll(
|
||||
pollStartId = existingPollId,
|
||||
question = question,
|
||||
answers = answers,
|
||||
maxSelections = maxSelections,
|
||||
pollKind = pollKind,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deletePoll(
|
||||
pollStartId: EventId,
|
||||
): Result<Unit> =
|
||||
timelineProvider
|
||||
.getActiveTimeline()
|
||||
.redactEvent(
|
||||
eventOrTransactionId = pollStartId.toEventOrTransactionId(),
|
||||
reason = null,
|
||||
)
|
||||
getTimelineProvider().flatMap { timelineProvider ->
|
||||
timelineProvider.getActiveTimeline()
|
||||
.redactEvent(
|
||||
eventOrTransactionId = pollStartId.toEventOrTransactionId(),
|
||||
reason = null,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getTimelineProvider(): Result<TimelineProvider> {
|
||||
return when (timelineMode) {
|
||||
is Timeline.Mode.Thread -> {
|
||||
val threadedTimelineResult = room.createTimeline(CreateTimelineParams.Threaded(timelineMode.threadRootId))
|
||||
threadedTimelineResult.map { threadedTimeline ->
|
||||
object : TimelineProvider {
|
||||
private val flow = MutableStateFlow<Timeline?>(threadedTimeline)
|
||||
override fun activeTimelineFlow(): StateFlow<Timeline?> = flow
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Result.success(defaultTimelineProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@@ -52,7 +53,12 @@ class PollHistoryFlowNode @AssistedInject constructor(
|
||||
return when (navTarget) {
|
||||
is NavTarget.EditPoll -> {
|
||||
createPollEntryPoint.nodeBuilder(this, buildContext)
|
||||
.params(CreatePollEntryPoint.Params(mode = CreatePollMode.EditPoll(eventId = navTarget.pollStartEventId)))
|
||||
.params(
|
||||
CreatePollEntryPoint.Params(
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
mode = CreatePollMode.EditPoll(eventId = navTarget.pollStartEventId)
|
||||
)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
NavTarget.Root -> {
|
||||
|
||||
@@ -67,10 +67,14 @@ class PollHistoryPresenter @Inject constructor(
|
||||
coroutineScope.loadMore(timeline)
|
||||
}
|
||||
is PollHistoryEvents.SelectPollAnswer -> sessionCoroutineScope.launch {
|
||||
sendPollResponseAction.execute(pollStartId = event.pollStartId, answerId = event.answerId)
|
||||
sendPollResponseAction.execute(
|
||||
timeline = timeline,
|
||||
pollStartId = event.pollStartId,
|
||||
answerId = event.answerId
|
||||
)
|
||||
}
|
||||
is PollHistoryEvents.EndPoll -> sessionCoroutineScope.launch {
|
||||
endPollAction.execute(pollStartId = event.pollStartId)
|
||||
endPollAction.execute(timeline = timeline, pollStartId = event.pollStartId)
|
||||
}
|
||||
is PollHistoryEvents.SelectFilter -> {
|
||||
activeFilter = event.filter
|
||||
|
||||
@@ -21,6 +21,7 @@ import io.element.android.features.poll.impl.anOngoingPollContent
|
||||
import io.element.android.features.poll.impl.data.PollRepository
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
@@ -551,12 +552,18 @@ class CreatePollPresenterTest {
|
||||
private fun createCreatePollPresenter(
|
||||
mode: CreatePollMode = CreatePollMode.NewPoll,
|
||||
room: FakeJoinedRoom = fakeJoinedRoom,
|
||||
timelineMode: Timeline.Mode = Timeline.Mode.Live,
|
||||
): CreatePollPresenter = CreatePollPresenter(
|
||||
repository = PollRepository(room, LiveTimelineProvider(room)),
|
||||
repositoryFactory = object : PollRepository.Factory {
|
||||
override fun create(timelineMode: Timeline.Mode): PollRepository {
|
||||
return PollRepository(room, LiveTimelineProvider(room), timelineMode)
|
||||
}
|
||||
},
|
||||
analyticsService = fakeAnalyticsService,
|
||||
messageComposerContext = fakeMessageComposerContext,
|
||||
navigateUp = { navUpInvocationsCount++ },
|
||||
mode = mode,
|
||||
timelineMode = timelineMode,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ package io.element.android.features.poll.test.actions
|
||||
|
||||
import io.element.android.features.poll.api.actions.EndPollAction
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
||||
class FakeEndPollAction : EndPollAction {
|
||||
private var executionCount = 0
|
||||
@@ -17,7 +18,7 @@ class FakeEndPollAction : EndPollAction {
|
||||
assert(executionCount == count)
|
||||
}
|
||||
|
||||
override suspend fun execute(pollStartId: EventId): Result<Unit> {
|
||||
override suspend fun execute(timeline: Timeline, pollStartId: EventId): Result<Unit> {
|
||||
executionCount++
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package io.element.android.features.poll.test.actions
|
||||
|
||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
|
||||
class FakeSendPollResponseAction : SendPollResponseAction {
|
||||
private var executionCount = 0
|
||||
@@ -17,7 +18,7 @@ class FakeSendPollResponseAction : SendPollResponseAction {
|
||||
assert(executionCount == count)
|
||||
}
|
||||
|
||||
override suspend fun execute(pollStartId: EventId, answerId: String): Result<Unit> {
|
||||
override suspend fun execute(timeline: Timeline, pollStartId: EventId, answerId: String): Result<Unit> {
|
||||
executionCount++
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user