Skip to content

Plain text editor implementation based on markdown input #2840

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .maestro/tests/roomList/timeline/messages/text.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ appId: ${MAESTRO_APP_ID}
---
- takeScreenshot: build/maestro/510-Timeline
- tapOn:
id: "rich_text_editor"
id: "text_editor"
- inputText: "Hello world!"
- tapOn: "Send"
- hideKeyboard
Expand Down
2 changes: 1 addition & 1 deletion .maestro/tests/settings/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ appId: ${MAESTRO_APP_ID}

- tapOn:
text: "Advanced settings"
- assertVisible: "Rich text editor"
- assertVisible: "View source"
- back

- tapOn:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.element.android.appconfig

object MessageComposerConfig {
/**
* Enable the rich text editing in the composer.
*/
const val ENABLE_RICH_TEXT_EDITING = true
}
1 change: 1 addition & 0 deletions changelog.d/2840.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add plain text editor based on Markdown input.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.appconfig.MessageComposerConfig
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
Expand Down Expand Up @@ -66,7 +67,6 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
Expand Down Expand Up @@ -113,7 +113,6 @@ class MessagesPresenter @AssistedInject constructor(
private val messageSummaryFormatter: MessageSummaryFormatter,
private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper,
private val appPreferencesStore: AppPreferencesStore,
private val featureFlagsService: FeatureFlagService,
private val htmlConverterProvider: HtmlConverterProvider,
@Assisted private val navigator: MessagesNavigator,
Expand Down Expand Up @@ -171,17 +170,15 @@ class MessagesPresenter @AssistedInject constructor(

val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
var showReinvitePrompt by remember { mutableStateOf(false) }
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow.value) {
LaunchedEffect(hasDismissedInviteDialog, composerState.textEditorState.hasFocus(), syncUpdateFlow.value) {
withContext(dispatchers.io) {
showReinvitePrompt = !hasDismissedInviteDialog && composerState.hasFocus && room.isDirect && room.activeMemberCount == 1L
showReinvitePrompt = !hasDismissedInviteDialog && composerState.textEditorState.hasFocus() && room.isDirect && room.activeMemberCount == 1L
}
}
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()

val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()

val enableTextFormatting by appPreferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)

var enableVoiceMessages by remember { mutableStateOf(false) }
LaunchedEffect(featureFlagsService) {
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
Expand All @@ -194,7 +191,7 @@ class MessagesPresenter @AssistedInject constructor(
action = event.action,
targetEvent = event.event,
composerState = composerState,
enableTextFormatting = enableTextFormatting,
enableTextFormatting = composerState.showTextFormatting,
timelineState = timelineState,
)
}
Expand Down Expand Up @@ -239,7 +236,7 @@ class MessagesPresenter @AssistedInject constructor(
snackbarMessage = snackbarMessage,
showReinvitePrompt = showReinvitePrompt,
inviteProgress = inviteProgress.value,
enableTextFormatting = enableTextFormatting,
enableTextFormatting = MessageComposerConfig.ENABLE_RICH_TEXT_EDITING,
enableVoiceMessages = enableVoiceMessages,
appName = buildMeta.applicationName,
callState = callState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.textcomposer.aRichTextEditorState
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.TextEditorState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf

Expand Down Expand Up @@ -99,9 +100,9 @@ fun aMessagesState(
userHasPermissionToRedactOther: Boolean = false,
userHasPermissionToSendReaction: Boolean = true,
composerState: MessageComposerState = aMessageComposerState(
richTextEditorState = aRichTextEditorState(initialText = "Hello", initialFocus = true),
isFullScreen = false,
mode = MessageComposerMode.Normal,
textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)),
isFullScreen = false,
mode = MessageComposerMode.Normal,
),
voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(),
timelineState: TimelineState = aTimelineState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ private fun MessagesViewContent(
// Any state change that should trigger a height size should be added to the list of remembered values here.
val sheetResizeContentKey = remember { mutableIntStateOf(0) }
LaunchedEffect(
state.composerState.richTextEditorState.lineCount,
state.composerState.textEditorState.lineCount,
state.composerState.showTextFormatting,
) {
sheetResizeContentKey.intValue = Random.nextInt()
Expand Down Expand Up @@ -439,7 +439,6 @@ private fun MessagesViewComposerBottomSheetContents(
state = state.composerState,
voiceMessageState = state.voiceMessageComposerState,
subcomposing = subcomposing,
enableTextFormatting = state.enableTextFormatting,
enableVoiceMessages = state.enableVoiceMessages,
modifier = Modifier.fillMaxWidth(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

Expand All @@ -51,8 +52,8 @@ fun MentionSuggestionsPickerView(
roomId: RoomId,
roomName: String?,
roomAvatarData: AvatarData?,
memberSuggestions: ImmutableList<MentionSuggestion>,
onSuggestionSelected: (MentionSuggestion) -> Unit,
memberSuggestions: ImmutableList<ResolvedMentionSuggestion>,
onSuggestionSelected: (ResolvedMentionSuggestion) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
Expand All @@ -62,8 +63,8 @@ fun MentionSuggestionsPickerView(
memberSuggestions,
key = { suggestion ->
when (suggestion) {
is MentionSuggestion.Room -> "@room"
is MentionSuggestion.Member -> suggestion.roomMember.userId.value
is ResolvedMentionSuggestion.AtRoom -> "@room"
is ResolvedMentionSuggestion.Member -> suggestion.roomMember.userId.value
}
}
) {
Expand All @@ -84,32 +85,32 @@ fun MentionSuggestionsPickerView(

@Composable
private fun RoomMemberSuggestionItemView(
memberSuggestion: MentionSuggestion,
memberSuggestion: ResolvedMentionSuggestion,
roomId: String,
roomName: String?,
roomAvatar: AvatarData?,
onSuggestionSelected: (MentionSuggestion) -> Unit,
onSuggestionSelected: (ResolvedMentionSuggestion) -> Unit,
modifier: Modifier = Modifier,
) {
Row(modifier = modifier.clickable { onSuggestionSelected(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
val avatarSize = AvatarSize.TimelineRoom
val avatarData = when (memberSuggestion) {
is MentionSuggestion.Room -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
is MentionSuggestion.Member -> AvatarData(
is ResolvedMentionSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
is ResolvedMentionSuggestion.Member -> AvatarData(
memberSuggestion.roomMember.userId.value,
memberSuggestion.roomMember.displayName,
memberSuggestion.roomMember.avatarUrl,
avatarSize,
)
}
val title = when (memberSuggestion) {
is MentionSuggestion.Room -> stringResource(R.string.screen_room_mentions_at_room_title)
is MentionSuggestion.Member -> memberSuggestion.roomMember.displayName
is ResolvedMentionSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.displayName
}

val subtitle = when (memberSuggestion) {
is MentionSuggestion.Room -> "@room"
is MentionSuggestion.Member -> memberSuggestion.roomMember.userId.value
is ResolvedMentionSuggestion.AtRoom -> "@room"
is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.userId.value
}

Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp))
Expand Down Expand Up @@ -159,9 +160,9 @@ internal fun MentionSuggestionsPickerViewPreview() {
roomName = "Room",
roomAvatarData = null,
memberSuggestions = persistentListOf(
MentionSuggestion.Room,
MentionSuggestion.Member(roomMember),
MentionSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
ResolvedMentionSuggestion.AtRoom,
ResolvedMentionSuggestion.Member(roomMember),
ResolvedMentionSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
),
onSuggestionSelected = {}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
import io.element.android.libraries.textcomposer.model.Suggestion
import io.element.android.libraries.textcomposer.model.SuggestionType

Expand All @@ -45,7 +46,7 @@ object MentionSuggestionsProcessor {
roomMembersState: MatrixRoomMembersState,
currentUserId: UserId,
canSendRoomMention: suspend () -> Boolean,
): List<MentionSuggestion> {
): List<ResolvedMentionSuggestion> {
val members = roomMembersState.roomMembers()
return when {
members.isNullOrEmpty() || suggestion == null -> {
Expand Down Expand Up @@ -78,7 +79,7 @@ object MentionSuggestionsProcessor {
roomMembers: List<RoomMember>?,
currentUserId: UserId,
canSendRoomMention: Boolean,
): List<MentionSuggestion> {
): List<ResolvedMentionSuggestion> {
return if (roomMembers.isNullOrEmpty()) {
emptyList()
} else {
Expand All @@ -96,10 +97,10 @@ object MentionSuggestionsProcessor {
.filterUpTo(MAX_BATCH_ITEMS) { member ->
isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query)
}
.map(MentionSuggestion::Member)
.map(ResolvedMentionSuggestion::Member)

if ("room".contains(query) && canSendRoomMention) {
listOf(MentionSuggestion.Room) + matchingMembers
listOf(ResolvedMentionSuggestion.AtRoom) + matchingMembers
} else {
matchingMembers
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ package io.element.android.features.messages.impl.messagecomposer

import android.net.Uri
import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.mentions.MentionSuggestion
import io.element.android.libraries.textcomposer.model.Message
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.Suggestion

@Immutable
sealed interface MessageComposerEvents {
data object ToggleFullScreenState : MessageComposerEvents
data class SendMessage(val message: Message) : MessageComposerEvents
data object SendMessage : MessageComposerEvents
data class SendUri(val uri: Uri) : MessageComposerEvents
data object CloseSpecialMode : MessageComposerEvents
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
Expand All @@ -45,5 +44,5 @@ sealed interface MessageComposerEvents {
data class Error(val error: Throwable) : MessageComposerEvents
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents
data class InsertMention(val mention: ResolvedMentionSuggestion) : MessageComposerEvents
}
Loading
Loading