Skip to content

Commit e0cf4cf

Browse files
authored
Merge pull request #3099 from element-hq/feature/fga/draft_support
Feature : Draft support
2 parents 81a5394 + 76d0f4b commit e0cf4cf

File tree

473 files changed

+1410
-762
lines changed

Some content is hidden

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

473 files changed

+1410
-762
lines changed

changelog.d/2869.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Store and restore drafts for each room.

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,8 @@ class SendLocationPresenterTest {
379379
fakeMessageComposerContext.apply {
380380
composerMode = MessageComposerMode.Edit(
381381
eventId = null,
382-
defaultContent = "",
383-
transactionId = null
382+
transactionId = null,
383+
content = ""
384384
)
385385
}
386386

@@ -427,8 +427,8 @@ class SendLocationPresenterTest {
427427
fakeMessageComposerContext.apply {
428428
composerMode = MessageComposerMode.Edit(
429429
eventId = null,
430-
defaultContent = "",
431-
transactionId = null
430+
transactionId = null,
431+
content = ""
432432
)
433433
}
434434

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
2626
import androidx.compose.runtime.setValue
2727
import androidx.compose.ui.Modifier
2828
import androidx.compose.ui.platform.LocalContext
29+
import androidx.lifecycle.Lifecycle
2930
import com.bumble.appyx.core.lifecycle.subscribe
3031
import com.bumble.appyx.core.modality.BuildContext
3132
import com.bumble.appyx.core.node.Node
@@ -35,6 +36,7 @@ import dagger.assisted.Assisted
3536
import dagger.assisted.AssistedInject
3637
import io.element.android.anvilannotations.ContributesNode
3738
import io.element.android.features.messages.impl.attachments.Attachment
39+
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
3840
import io.element.android.features.messages.impl.timeline.TimelineController
3941
import io.element.android.features.messages.impl.timeline.TimelineEvents
4042
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
@@ -45,6 +47,7 @@ import io.element.android.libraries.androidutils.system.toast
4547
import io.element.android.libraries.architecture.NodeInputs
4648
import io.element.android.libraries.architecture.inputs
4749
import io.element.android.libraries.core.bool.orFalse
50+
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
4851
import io.element.android.libraries.di.ApplicationContext
4952
import io.element.android.libraries.di.RoomScope
5053
import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
@@ -195,6 +198,12 @@ class MessagesNode @AssistedInject constructor(
195198
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
196199
) {
197200
val state = presenter.present()
201+
OnLifecycleEvent { _, event ->
202+
when (event) {
203+
Lifecycle.Event.ON_PAUSE -> state.composerState.eventSink(MessageComposerEvents.SaveDraft)
204+
else -> Unit
205+
}
206+
}
198207
MessagesView(
199208
state = state,
200209
onBackClick = this::navigateUp,

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

Lines changed: 11 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,10 @@ import io.element.android.features.messages.impl.timeline.components.customreact
4747
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
4848
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter
4949
import io.element.android.features.messages.impl.timeline.model.TimelineItem
50-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
51-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
52-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
53-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
54-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
55-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
56-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
5750
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
58-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
5951
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
60-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
6152
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
62-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
63-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
64-
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
6553
import io.element.android.features.messages.impl.typing.TypingNotificationPresenter
66-
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
6754
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
6855
import io.element.android.features.networkmonitor.api.NetworkMonitor
6956
import io.element.android.features.networkmonitor.api.NetworkStatus
@@ -80,12 +67,12 @@ import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarM
8067
import io.element.android.libraries.featureflag.api.FeatureFlagService
8168
import io.element.android.libraries.featureflag.api.FeatureFlags
8269
import io.element.android.libraries.matrix.api.core.EventId
70+
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
8371
import io.element.android.libraries.matrix.api.room.MatrixRoom
8472
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
8573
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
8674
import io.element.android.libraries.matrix.api.room.MessageEventType
87-
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
88-
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
75+
import io.element.android.libraries.matrix.ui.messages.reply.map
8976
import io.element.android.libraries.matrix.ui.model.getAvatarData
9077
import io.element.android.libraries.matrix.ui.room.canCall
9178
import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState
@@ -100,6 +87,7 @@ import kotlinx.coroutines.withContext
10087
import timber.log.Timber
10188

10289
class MessagesPresenter @AssistedInject constructor(
90+
@Assisted private val navigator: MessagesNavigator,
10391
private val room: MatrixRoom,
10492
private val composerPresenter: MessageComposerPresenter,
10593
private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter,
@@ -111,14 +99,13 @@ class MessagesPresenter @AssistedInject constructor(
11199
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
112100
private val networkMonitor: NetworkMonitor,
113101
private val snackbarDispatcher: SnackbarDispatcher,
114-
private val messageSummaryFormatter: MessageSummaryFormatter,
115102
private val dispatchers: CoroutineDispatchers,
116103
private val clipboardHelper: ClipboardHelper,
117104
private val featureFlagsService: FeatureFlagService,
118105
private val htmlConverterProvider: HtmlConverterProvider,
119-
@Assisted private val navigator: MessagesNavigator,
120106
private val buildMeta: BuildMeta,
121107
private val timelineController: TimelineController,
108+
private val permalinkParser: PermalinkParser,
122109
) : Presenter<MessagesState> {
123110
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
124111

@@ -336,14 +323,14 @@ class MessagesPresenter @AssistedInject constructor(
336323
else -> {
337324
val composerMode = MessageComposerMode.Edit(
338325
targetEvent.eventId,
326+
targetEvent.transactionId,
339327
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
340328
if (enableTextFormatting) {
341329
it.htmlBody ?: it.body
342330
} else {
343331
it.body
344332
}
345333
}.orEmpty(),
346-
targetEvent.transactionId,
347334
)
348335
composerState.eventSink(
349336
MessageComposerEvents.SetMode(composerMode)
@@ -352,66 +339,15 @@ class MessagesPresenter @AssistedInject constructor(
352339
}
353340
}
354341

355-
private fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
342+
private suspend fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
356343
if (targetEvent.eventId == null) return
357-
val textContent = messageSummaryFormatter.format(targetEvent)
358-
val attachmentThumbnailInfo = when (targetEvent.content) {
359-
is TimelineItemImageContent -> AttachmentThumbnailInfo(
360-
thumbnailSource = targetEvent.content.thumbnailSource ?: targetEvent.content.mediaSource,
361-
textContent = targetEvent.content.body,
362-
type = AttachmentThumbnailType.Image,
363-
blurHash = targetEvent.content.blurhash,
364-
)
365-
is TimelineItemStickerContent -> AttachmentThumbnailInfo(
366-
thumbnailSource = targetEvent.content.thumbnailSource ?: targetEvent.content.mediaSource,
367-
textContent = targetEvent.content.body,
368-
type = AttachmentThumbnailType.Image,
369-
blurHash = targetEvent.content.blurhash,
370-
)
371-
is TimelineItemVideoContent -> AttachmentThumbnailInfo(
372-
thumbnailSource = targetEvent.content.thumbnailSource,
373-
textContent = targetEvent.content.body,
374-
type = AttachmentThumbnailType.Video,
375-
blurHash = targetEvent.content.blurHash,
376-
)
377-
is TimelineItemFileContent -> AttachmentThumbnailInfo(
378-
thumbnailSource = targetEvent.content.thumbnailSource,
379-
textContent = targetEvent.content.body,
380-
type = AttachmentThumbnailType.File,
381-
)
382-
is TimelineItemAudioContent -> AttachmentThumbnailInfo(
383-
textContent = targetEvent.content.body,
384-
type = AttachmentThumbnailType.Audio,
385-
)
386-
is TimelineItemVoiceContent -> AttachmentThumbnailInfo(
387-
textContent = textContent,
388-
type = AttachmentThumbnailType.Voice,
389-
)
390-
is TimelineItemLocationContent -> AttachmentThumbnailInfo(
391-
type = AttachmentThumbnailType.Location,
392-
)
393-
is TimelineItemPollContent -> AttachmentThumbnailInfo(
394-
textContent = targetEvent.content.question,
395-
type = AttachmentThumbnailType.Poll,
344+
timelineController.invokeOnCurrentTimeline {
345+
val replyToDetails = loadReplyDetails(targetEvent.eventId).map(permalinkParser)
346+
val composerMode = MessageComposerMode.Reply(replyToDetails = replyToDetails)
347+
composerState.eventSink(
348+
MessageComposerEvents.SetMode(composerMode)
396349
)
397-
is TimelineItemTextBasedContent,
398-
is TimelineItemRedactedContent,
399-
is TimelineItemStateContent,
400-
is TimelineItemEncryptedContent,
401-
is TimelineItemLegacyCallInviteContent,
402-
is TimelineItemCallNotifyContent,
403-
is TimelineItemUnknownContent -> null
404350
}
405-
val composerMode = MessageComposerMode.Reply(
406-
isThreaded = targetEvent.isThreaded,
407-
senderName = targetEvent.safeSenderName,
408-
eventId = targetEvent.eventId,
409-
attachmentThumbnailInfo = attachmentThumbnailInfo,
410-
defaultContent = textContent,
411-
)
412-
composerState.eventSink(
413-
MessageComposerEvents.SetMode(composerMode)
414-
)
415351
}
416352

417353
private fun handleShowDebugInfoAction(event: TimelineItem.Event) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ import androidx.compose.ui.unit.dp
5555
import io.element.android.compound.theme.ElementTheme
5656
import io.element.android.compound.tokens.generated.CompoundIcons
5757
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
58-
import io.element.android.features.messages.impl.sender.SenderName
59-
import io.element.android.features.messages.impl.sender.SenderNameMode
6058
import io.element.android.features.messages.impl.timeline.model.TimelineItem
6159
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
6260
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
@@ -88,6 +86,8 @@ import io.element.android.libraries.designsystem.theme.components.ListItemStyle
8886
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
8987
import io.element.android.libraries.designsystem.theme.components.Text
9088
import io.element.android.libraries.designsystem.theme.components.hide
89+
import io.element.android.libraries.matrix.ui.messages.sender.SenderName
90+
import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode
9191
import io.element.android.libraries.ui.strings.CommonStrings
9292
import kotlinx.collections.immutable.ImmutableList
9393

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+
* http://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.draft
18+
19+
import io.element.android.libraries.matrix.api.core.RoomId
20+
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
21+
22+
interface ComposerDraftService {
23+
suspend fun loadDraft(roomId: RoomId): ComposerDraft?
24+
suspend fun saveDraft(roomId: RoomId, draft: ComposerDraft)
25+
}
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+
* http://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.draft
18+
19+
import com.squareup.anvil.annotations.ContributesBinding
20+
import io.element.android.libraries.di.RoomScope
21+
import io.element.android.libraries.matrix.api.MatrixClient
22+
import io.element.android.libraries.matrix.api.core.RoomId
23+
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
24+
import timber.log.Timber
25+
import javax.inject.Inject
26+
27+
@ContributesBinding(RoomScope::class)
28+
class DefaultComposerDraftService @Inject constructor(
29+
private val client: MatrixClient,
30+
) : ComposerDraftService {
31+
override suspend fun loadDraft(roomId: RoomId): ComposerDraft? {
32+
return client.getRoom(roomId)?.use { room ->
33+
room.loadComposerDraft()
34+
.onFailure {
35+
Timber.e(it, "Failed to load composer draft for room $roomId")
36+
}
37+
.onSuccess { draft ->
38+
room.clearComposerDraft()
39+
Timber.d("Loaded composer draft for room $roomId : $draft")
40+
}
41+
.getOrNull()
42+
}
43+
}
44+
45+
override suspend fun saveDraft(roomId: RoomId, draft: ComposerDraft) {
46+
client.getRoom(roomId)?.use { room ->
47+
room.saveComposerDraft(draft)
48+
.onFailure {
49+
Timber.e(it, "Failed to save composer draft for room $roomId")
50+
}
51+
.onSuccess {
52+
Timber.d("Saved composer draft for room $roomId")
53+
}
54+
}
55+
}
56+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ sealed interface MessageComposerEvents {
4545
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
4646
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
4747
data class InsertMention(val mention: ResolvedMentionSuggestion) : MessageComposerEvents
48+
data object SaveDraft : MessageComposerEvents
4849
}

0 commit comments

Comments
 (0)