Merge branch 'develop' into feature/fga/sync_indicator_api
This commit is contained in:
commit
555f706fbb
108 changed files with 558 additions and 406 deletions
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
|
|
@ -22,6 +22,16 @@ jobs:
|
|||
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
# Increase swapfile size to prevent screenshot tests getting terminated
|
||||
# https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749
|
||||
- name: 💽 Increase swapfile size
|
||||
run: |
|
||||
sudo swapoff -a
|
||||
sudo fallocate -l 8G /mnt/swapfile
|
||||
sudo chmod 600 /mnt/swapfile
|
||||
sudo mkswap /mnt/swapfile
|
||||
sudo swapon /mnt/swapfile
|
||||
sudo swapon --show
|
||||
- name: ⏬ Checkout with LFS
|
||||
uses: nschloe/action-cached-lfs-checkout@v1.2.2
|
||||
with:
|
||||
|
|
|
|||
2
changelog.d/1172.feature
Normal file
2
changelog.d/1172.feature
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[Rich text editor] Integrate rich text editor library. Note that markdown is now not supported and further formatting support will be introduced through the rich text editor.
|
||||
|
||||
1
changelog.d/1251.misc
Normal file
1
changelog.d/1251.misc
Normal file
|
|
@ -0,0 +1 @@
|
|||
Improve RoomSummary mapping by using RoomInfo.
|
||||
|
|
@ -25,5 +25,5 @@ android {
|
|||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.libraries.textcomposer)
|
||||
api(projects.libraries.textcomposer.impl)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ dependencies {
|
|||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.textcomposer)
|
||||
implementation(projects.libraries.textcomposer.impl)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.dateformatter.api)
|
||||
implementation(projects.libraries.eventformatter.api)
|
||||
|
|
@ -76,6 +76,7 @@ dependencies {
|
|||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.libraries.mediapickers.test)
|
||||
testImplementation(projects.libraries.textcomposer.test)
|
||||
testImplementation(libs.test.mockk)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
snackbarMessage = snackbarMessage,
|
||||
showReinvitePrompt = showReinvitePrompt,
|
||||
inviteProgress = inviteProgress.value,
|
||||
eventSink = ::handleEvents
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -250,7 +250,9 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
||||
val composerMode = MessageComposerMode.Edit(
|
||||
targetEvent.eventId,
|
||||
(targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty(),
|
||||
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
|
||||
it.htmlBody ?: it.body
|
||||
}.orEmpty(),
|
||||
targetEvent.transactionId,
|
||||
)
|
||||
composerState.eventSink(
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||
|
|
@ -54,7 +55,9 @@ fun aMessagesState() = MessagesState(
|
|||
userHasPermissionToSendMessage = true,
|
||||
userHasPermissionToRedact = false,
|
||||
composerState = aMessageComposerState().copy(
|
||||
text = "Hello",
|
||||
richTextEditorState = RichTextEditorState("Hello", fake = true).apply {
|
||||
requestFocus()
|
||||
},
|
||||
isFullScreen = false,
|
||||
mode = MessageComposerMode.Normal("Hello"),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class ActionListPresenter @Inject constructor(
|
|||
return ActionListState(
|
||||
target = target.value,
|
||||
displayEmojiReactions = displayEmojiReactions,
|
||||
eventSink = ::handleEvents
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,16 +17,15 @@
|
|||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.textcomposer.Message
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
|
||||
@Immutable
|
||||
sealed interface MessageComposerEvents {
|
||||
data object ToggleFullScreenState : MessageComposerEvents
|
||||
data class FocusChanged(val hasFocus: Boolean) : MessageComposerEvents
|
||||
data class SendMessage(val message: String) : MessageComposerEvents
|
||||
data class SendMessage(val message: Message) : MessageComposerEvents
|
||||
data object CloseSpecialMode : MessageComposerEvents
|
||||
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
|
||||
data class UpdateText(val text: String) : MessageComposerEvents
|
||||
data object AddAttachment : MessageComposerEvents
|
||||
data object DismissAttachmentMenu : MessageComposerEvents
|
||||
sealed interface PickAttachmentSource : MessageComposerEvents {
|
||||
|
|
@ -38,4 +37,5 @@ sealed interface MessageComposerEvents {
|
|||
data object Poll : PickAttachmentSource
|
||||
}
|
||||
data object CancelSendAttachment : MessageComposerEvents
|
||||
data class Error(val error: Throwable) : MessageComposerEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,10 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback
|
|||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.textcomposer.Message
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -67,6 +69,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val messageComposerContext: MessageComposerContextImpl,
|
||||
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
||||
) : Presenter<MessageComposerState> {
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
|
|
@ -103,19 +106,15 @@ class MessageComposerPresenter @Inject constructor(
|
|||
val isFullScreen = rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val hasFocus = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val text: MutableState<String> = rememberSaveable {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val richTextEditorState = richTextEditorStateFactory.create()
|
||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(messageComposerContext.composerMode) {
|
||||
when (val modeValue = messageComposerContext.composerMode) {
|
||||
is MessageComposerMode.Edit -> text.value = modeValue.defaultContent
|
||||
is MessageComposerMode.Edit ->
|
||||
richTextEditorState.setHtml(modeValue.defaultContent)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -136,18 +135,15 @@ class MessageComposerPresenter @Inject constructor(
|
|||
when (event) {
|
||||
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
|
||||
|
||||
is MessageComposerEvents.FocusChanged -> hasFocus.value = event.hasFocus
|
||||
|
||||
is MessageComposerEvents.UpdateText -> text.value = event.text
|
||||
MessageComposerEvents.CloseSpecialMode -> {
|
||||
text.value = ""
|
||||
richTextEditorState.setHtml("")
|
||||
messageComposerContext.composerMode = MessageComposerMode.Normal("")
|
||||
}
|
||||
|
||||
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(
|
||||
text = event.message,
|
||||
message = event.message,
|
||||
updateComposerMode = { messageComposerContext.composerMode = it },
|
||||
textState = text
|
||||
richTextEditorState = richTextEditorState,
|
||||
)
|
||||
is MessageComposerEvents.SetMode -> {
|
||||
messageComposerContext.composerMode = event.composerMode
|
||||
|
|
@ -194,43 +190,46 @@ class MessageComposerPresenter @Inject constructor(
|
|||
ongoingSendAttachmentJob.value == null
|
||||
}
|
||||
}
|
||||
is MessageComposerEvents.Error -> {
|
||||
analyticsService.trackError(event.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MessageComposerState(
|
||||
text = text.value,
|
||||
richTextEditorState = richTextEditorState,
|
||||
isFullScreen = isFullScreen.value,
|
||||
hasFocus = hasFocus.value,
|
||||
mode = messageComposerContext.composerMode,
|
||||
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
||||
canShareLocation = canShareLocation.value,
|
||||
canCreatePoll = canCreatePoll.value,
|
||||
attachmentsState = attachmentsState.value,
|
||||
eventSink = ::handleEvents
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.sendMessage(
|
||||
text: String,
|
||||
message: Message,
|
||||
updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit,
|
||||
textState: MutableState<String>
|
||||
richTextEditorState: RichTextEditorState,
|
||||
) = launch {
|
||||
val capturedMode = messageComposerContext.composerMode
|
||||
// Reset composer right away
|
||||
textState.value = ""
|
||||
richTextEditorState.setHtml("")
|
||||
updateComposerMode(MessageComposerMode.Normal(""))
|
||||
when (capturedMode) {
|
||||
is MessageComposerMode.Normal -> room.sendMessage(text)
|
||||
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html)
|
||||
is MessageComposerMode.Edit -> {
|
||||
val eventId = capturedMode.eventId
|
||||
val transactionId = capturedMode.transactionId
|
||||
room.editMessage(eventId, transactionId, text)
|
||||
room.editMessage(eventId, transactionId, message.markdown, message.html)
|
||||
}
|
||||
|
||||
is MessageComposerMode.Quote -> TODO()
|
||||
is MessageComposerMode.Reply -> room.replyMessage(
|
||||
capturedMode.eventId,
|
||||
text
|
||||
message.markdown,
|
||||
message.html,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,21 +19,22 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class MessageComposerState(
|
||||
val text: String?,
|
||||
val richTextEditorState: RichTextEditorState,
|
||||
val isFullScreen: Boolean,
|
||||
val hasFocus: Boolean,
|
||||
val mode: MessageComposerMode,
|
||||
val showAttachmentSourcePicker: Boolean,
|
||||
val canShareLocation: Boolean,
|
||||
val canCreatePoll: Boolean,
|
||||
val attachmentsState: AttachmentsState,
|
||||
val eventSink: (MessageComposerEvents) -> Unit
|
||||
val eventSink: (MessageComposerEvents) -> Unit,
|
||||
) {
|
||||
val isSendButtonVisible: Boolean = text.isNullOrEmpty().not()
|
||||
val canSendMessage: Boolean = richTextEditorState.messageHtml.isNotEmpty()
|
||||
val hasFocus: Boolean = richTextEditorState.hasFocus
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
|
||||
open class MessageComposerStateProvider : PreviewParameterProvider<MessageComposerState> {
|
||||
override val values: Sequence<MessageComposerState>
|
||||
|
|
@ -27,18 +28,17 @@ open class MessageComposerStateProvider : PreviewParameterProvider<MessageCompos
|
|||
}
|
||||
|
||||
fun aMessageComposerState(
|
||||
text: String = "",
|
||||
requestFocus: Boolean = true,
|
||||
composerState: RichTextEditorState = RichTextEditorState("", fake = true),
|
||||
isFullScreen: Boolean = false,
|
||||
hasFocus: Boolean = false,
|
||||
mode: MessageComposerMode = MessageComposerMode.Normal(content = ""),
|
||||
showAttachmentSourcePicker: Boolean = false,
|
||||
canShareLocation: Boolean = true,
|
||||
canCreatePoll: Boolean = true,
|
||||
attachmentsState: AttachmentsState = AttachmentsState.None,
|
||||
) = MessageComposerState(
|
||||
text = text,
|
||||
richTextEditorState = composerState.apply { if(requestFocus) requestFocus() },
|
||||
isFullScreen = isFullScreen,
|
||||
hasFocus = hasFocus,
|
||||
mode = mode,
|
||||
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
||||
canShareLocation = canShareLocation,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.textcomposer.Message
|
||||
import io.element.android.libraries.textcomposer.TextComposer
|
||||
|
||||
@Composable
|
||||
|
|
@ -36,7 +37,7 @@ fun MessageComposerView(
|
|||
state.eventSink(MessageComposerEvents.ToggleFullScreenState)
|
||||
}
|
||||
|
||||
fun sendMessage(message: String) {
|
||||
fun sendMessage(message: Message) {
|
||||
state.eventSink(MessageComposerEvents.SendMessage(message))
|
||||
}
|
||||
|
||||
|
|
@ -48,12 +49,8 @@ fun MessageComposerView(
|
|||
state.eventSink(MessageComposerEvents.CloseSpecialMode)
|
||||
}
|
||||
|
||||
fun onComposerTextChange(text: String) {
|
||||
state.eventSink(MessageComposerEvents.UpdateText(text))
|
||||
}
|
||||
|
||||
fun onFocusChanged(hasFocus: Boolean) {
|
||||
state.eventSink(MessageComposerEvents.FocusChanged(hasFocus))
|
||||
fun onError(error: Throwable) {
|
||||
state.eventSink(MessageComposerEvents.Error(error))
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
|
|
@ -64,14 +61,14 @@ fun MessageComposerView(
|
|||
)
|
||||
|
||||
TextComposer(
|
||||
state = state.richTextEditorState,
|
||||
canSendMessage = state.canSendMessage,
|
||||
onRequestFocus = { state.richTextEditorState.requestFocus() },
|
||||
onSendMessage = ::sendMessage,
|
||||
composerMode = state.mode,
|
||||
onResetComposerMode = ::onCloseSpecialMode,
|
||||
onComposerTextChange = ::onComposerTextChange,
|
||||
onAddAttachment = ::onAddAttachment,
|
||||
onFocusChanged = ::onFocusChanged,
|
||||
composerCanSendMessage = state.isSendButtonVisible,
|
||||
composerText = state.text
|
||||
onError = ::onError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import io.element.android.wysiwyg.compose.rememberRichTextEditorState
|
||||
import javax.inject.Inject
|
||||
|
||||
interface RichTextEditorStateFactory {
|
||||
@Composable
|
||||
fun create(): RichTextEditorState
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRichTextEditorStateFactory @Inject constructor() : RichTextEditorStateFactory {
|
||||
@Composable
|
||||
override fun create(): RichTextEditorState {
|
||||
return rememberRichTextEditorState()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -61,7 +62,7 @@ class TimelinePresenter @Inject constructor(
|
|||
mutableStateOf(null)
|
||||
}
|
||||
|
||||
val lastReadReceiptIndex = rememberSaveable { mutableStateOf(Int.MAX_VALUE) }
|
||||
val lastReadReceiptIndex = rememberSaveable { mutableIntStateOf(Int.MAX_VALUE) }
|
||||
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
|
||||
|
||||
val timelineItems by timelineItemsFactory.collectItemsAsState()
|
||||
|
|
@ -119,7 +120,7 @@ class TimelinePresenter @Inject constructor(
|
|||
paginationState = paginationState,
|
||||
timelineItems = timelineItems,
|
||||
hasNewItems = hasNewItems.value,
|
||||
eventSink = ::handleEvents
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class CustomReactionPresenter @Inject constructor(
|
|||
return CustomReactionState(
|
||||
target = target.value,
|
||||
selectedEmoji = selectedEmoji,
|
||||
eventSink = ::handleEvents
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class ReactionSummaryPresenter @Inject constructor(
|
|||
}
|
||||
return ReactionSummaryState(
|
||||
target = targetWithAvatars.value,
|
||||
eventSink = ::handleEvents
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class RetrySendMenuPresenter @Inject constructor(
|
|||
|
||||
return RetrySendMenuState(
|
||||
selectedEvent = selectedEvent,
|
||||
eventSink = ::handleEvent,
|
||||
eventSink = { handleEvent(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,4 +22,6 @@ sealed interface TimelineItemTextBasedContent : TimelineItemEventContent {
|
|||
val body: String
|
||||
val htmlDocument: Document?
|
||||
val isEdited: Boolean
|
||||
val htmlBody: String?
|
||||
get() = htmlDocument?.body()?.html()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
|||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
||||
|
|
@ -41,6 +40,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||
import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory
|
||||
import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider
|
||||
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
|
||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||
|
|
@ -325,6 +325,7 @@ class MessagesPresenterTest {
|
|||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent()))
|
||||
assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID)
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
skipItems(1) // back paginating
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +382,7 @@ class MessagesPresenterTest {
|
|||
// Initially the composer doesn't have focus, so we don't show the alert
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
// When the input field is focused we show the alert
|
||||
initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true))
|
||||
initialState.composerState.richTextEditorState.requestFocus()
|
||||
val focusedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state ->
|
||||
state.showReinvitePrompt
|
||||
}.last()
|
||||
|
|
@ -405,7 +406,7 @@ class MessagesPresenterTest {
|
|||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true))
|
||||
initialState.composerState.richTextEditorState.requestFocus()
|
||||
val focusedState = awaitItem()
|
||||
assertThat(focusedState.showReinvitePrompt).isFalse()
|
||||
}
|
||||
|
|
@ -421,7 +422,7 @@ class MessagesPresenterTest {
|
|||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true))
|
||||
initialState.composerState.richTextEditorState.requestFocus()
|
||||
val focusedState = awaitItem()
|
||||
assertThat(focusedState.showReinvitePrompt).isFalse()
|
||||
}
|
||||
|
|
@ -605,6 +606,8 @@ class MessagesPresenterTest {
|
|||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
messageComposerContext = MessageComposerContextImpl(),
|
||||
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
||||
|
||||
)
|
||||
val timelinePresenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
|||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.textcomposer.Message
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
|
|
@ -80,6 +81,7 @@ class MessageComposerPresenterTest {
|
|||
private val snackbarDispatcher = SnackbarDispatcher()
|
||||
private val mockMediaUrl: Uri = mockk("localMediaUri")
|
||||
private val localMediaFactory = FakeLocalMediaFactory(mockMediaUrl)
|
||||
private val analyticsService = FakeAnalyticsService()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
|
|
@ -90,12 +92,12 @@ class MessageComposerPresenterTest {
|
|||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isFullScreen).isFalse()
|
||||
assertThat(initialState.text).isEqualTo("")
|
||||
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal(""))
|
||||
assertThat(initialState.showAttachmentSourcePicker).isFalse()
|
||||
assertThat(initialState.canShareLocation).isTrue()
|
||||
assertThat(initialState.attachmentsState).isEqualTo(AttachmentsState.None)
|
||||
assertThat(initialState.isSendButtonVisible).isFalse()
|
||||
assertThat(initialState.canSendMessage).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,14 +126,14 @@ class MessageComposerPresenterTest {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_MESSAGE))
|
||||
initialState.richTextEditorState.setHtml(A_MESSAGE)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(""))
|
||||
assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.canSendMessage).isTrue()
|
||||
withMessageState.richTextEditorState.setHtml("")
|
||||
val withEmptyMessageState = awaitItem()
|
||||
assertThat(withEmptyMessageState.text).isEqualTo("")
|
||||
assertThat(withEmptyMessageState.isSendButtonVisible).isFalse()
|
||||
assertThat(withEmptyMessageState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(withEmptyMessageState.canSendMessage).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +150,8 @@ class MessageComposerPresenterTest {
|
|||
state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
state = awaitItem()
|
||||
assertThat(state.text).isEqualTo(A_MESSAGE)
|
||||
assertThat(state.isSendButtonVisible).isTrue()
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
|
||||
assertThat(state.canSendMessage).isTrue()
|
||||
backToNormalMode(state, skipCount = 1)
|
||||
}
|
||||
}
|
||||
|
|
@ -166,8 +168,8 @@ class MessageComposerPresenterTest {
|
|||
state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
assertThat(state.text).isEqualTo("")
|
||||
assertThat(state.isSendButtonVisible).isFalse()
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(state.canSendMessage).isFalse()
|
||||
backToNormalMode(state)
|
||||
}
|
||||
}
|
||||
|
|
@ -184,8 +186,8 @@ class MessageComposerPresenterTest {
|
|||
state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
assertThat(state.text).isEqualTo("")
|
||||
assertThat(state.isSendButtonVisible).isFalse()
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(state.canSendMessage).isFalse()
|
||||
backToNormalMode(state)
|
||||
}
|
||||
}
|
||||
|
|
@ -198,14 +200,14 @@ class MessageComposerPresenterTest {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_MESSAGE))
|
||||
initialState.richTextEditorState.setHtml(A_MESSAGE)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_MESSAGE))
|
||||
assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.canSendMessage).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage()))
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.text).isEqualTo("")
|
||||
assertThat(messageSentState.isSendButtonVisible).isFalse()
|
||||
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(messageSentState.canSendMessage).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -221,23 +223,23 @@ class MessageComposerPresenterTest {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.text).isEqualTo("")
|
||||
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
val mode = anEditMode()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
skipItems(1)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.mode).isEqualTo(mode)
|
||||
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE))
|
||||
assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.canSendMessage).isTrue()
|
||||
withMessageState.richTextEditorState.setHtml(ANOTHER_MESSAGE)
|
||||
val withEditedMessageState = awaitItem()
|
||||
assertThat(withEditedMessageState.text).isEqualTo(ANOTHER_MESSAGE)
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE))
|
||||
assertThat(withEditedMessageState.richTextEditorState.messageHtml).isEqualTo(ANOTHER_MESSAGE)
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE.toMessage()))
|
||||
skipItems(1)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.text).isEqualTo("")
|
||||
assertThat(messageSentState.isSendButtonVisible).isFalse()
|
||||
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE)
|
||||
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(messageSentState.canSendMessage).isFalse()
|
||||
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -253,23 +255,23 @@ class MessageComposerPresenterTest {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.text).isEqualTo("")
|
||||
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
val mode = anEditMode(eventId = null, transactionId = A_TRANSACTION_ID)
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
skipItems(1)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.mode).isEqualTo(mode)
|
||||
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE))
|
||||
assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.canSendMessage).isTrue()
|
||||
withMessageState.richTextEditorState.setHtml(ANOTHER_MESSAGE)
|
||||
val withEditedMessageState = awaitItem()
|
||||
assertThat(withEditedMessageState.text).isEqualTo(ANOTHER_MESSAGE)
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE))
|
||||
assertThat(withEditedMessageState.richTextEditorState.messageHtml).isEqualTo(ANOTHER_MESSAGE)
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE.toMessage()))
|
||||
skipItems(1)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.text).isEqualTo("")
|
||||
assertThat(messageSentState.isSendButtonVisible).isFalse()
|
||||
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE)
|
||||
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(messageSentState.canSendMessage).isFalse()
|
||||
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -285,23 +287,23 @@ class MessageComposerPresenterTest {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.text).isEqualTo("")
|
||||
assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
val mode = aReplyMode()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
val state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
assertThat(state.text).isEqualTo("")
|
||||
assertThat(state.isSendButtonVisible).isFalse()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_REPLY))
|
||||
assertThat(state.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(state.canSendMessage).isFalse()
|
||||
state.richTextEditorState.setHtml(A_REPLY)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.text).isEqualTo(A_REPLY)
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY))
|
||||
assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_REPLY)
|
||||
assertThat(withMessageState.canSendMessage).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage()))
|
||||
skipItems(1)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.text).isEqualTo("")
|
||||
assertThat(messageSentState.isSendButtonVisible).isFalse()
|
||||
assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY)
|
||||
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(messageSentState.canSendMessage).isFalse()
|
||||
assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY to A_REPLY)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -523,13 +525,27 @@ class MessageComposerPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - errors are tracked`() = runTest {
|
||||
val testException = Exception("Test error")
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessageComposerEvents.Error(testException))
|
||||
assertThat(analyticsService.trackedErrors).containsExactly(testException)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) {
|
||||
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
|
||||
skipItems(skipCount)
|
||||
val normalState = awaitItem()
|
||||
assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal(""))
|
||||
assertThat(normalState.text).isEqualTo("")
|
||||
assertThat(normalState.isSendButtonVisible).isFalse()
|
||||
assertThat(normalState.richTextEditorState.messageHtml).isEqualTo("")
|
||||
assertThat(normalState.canSendMessage).isFalse()
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
|
|
@ -547,8 +563,9 @@ class MessageComposerPresenterTest {
|
|||
localMediaFactory,
|
||||
MediaSender(mediaPreProcessor, room),
|
||||
snackbarDispatcher,
|
||||
FakeAnalyticsService(),
|
||||
analyticsService,
|
||||
MessageComposerContextImpl(),
|
||||
TestRichTextEditorStateFactory(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -560,3 +577,8 @@ fun anEditMode(
|
|||
|
||||
fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, AN_EVENT_ID, A_MESSAGE)
|
||||
fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE)
|
||||
|
||||
private fun String.toMessage() = Message(
|
||||
html = this,
|
||||
markdown = this,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.features.messages.textcomposer
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.messages.impl.messagecomposer.RichTextEditorStateFactory
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import io.element.android.wysiwyg.compose.rememberRichTextEditorState
|
||||
|
||||
class TestRichTextEditorStateFactory : RichTextEditorStateFactory {
|
||||
@Composable
|
||||
override fun create(): RichTextEditorState {
|
||||
return rememberRichTextEditorState("", fake = true)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,10 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
|
|
@ -52,8 +54,8 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
|||
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
|
|
@ -210,6 +212,11 @@ fun RoomListContent(
|
|||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
// Add a last Spacer item to ensure that the FAB does not hide the last room item
|
||||
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(80.dp))
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
|
|
@ -219,8 +226,7 @@ fun RoomListContent(
|
|||
onClick = onCreateRoomClicked
|
||||
) {
|
||||
Icon(
|
||||
// Correct icon alignment for better rendering.
|
||||
modifier = Modifier.padding(start = 1.dp, bottom = 1.dp),
|
||||
// Note cannot use Icons.Outlined.EditSquare, it does not exist :/
|
||||
resourceId = DrawableR.drawable.ic_edit_square,
|
||||
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ dependencyanalysis = "1.21.0"
|
|||
stem = "2.3.0"
|
||||
sqldelight = "1.5.5"
|
||||
telephoto = "0.6.0"
|
||||
wysiwyg = "2.9.0"
|
||||
|
||||
# DI
|
||||
dagger = "2.48"
|
||||
|
|
@ -146,7 +147,9 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
|||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.49"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.50"
|
||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
||||
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
|
||||
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.theme.components
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
|
|
@ -38,7 +39,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
|||
fun FloatingActionButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = FloatingActionButtonDefaults.shape,
|
||||
shape: Shape = CircleShape, // FloatingActionButtonDefaults.shape,
|
||||
containerColor: Color = FloatingActionButtonDefaults.containerColor,
|
||||
contentColor: Color = contentColorFor(containerColor),
|
||||
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
||||
|
|
|
|||
|
|
@ -15,12 +15,13 @@
|
|||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:tint="#000000"
|
||||
android:width="21dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="21"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:pathData="M1.5,21.7C1.1,21.7 0.75,21.55 0.45,21.25C0.15,20.95 0,20.6 0,20.2V5.2C0,4.8 0.15,4.45 0.45,4.15C0.75,3.85 1.1,3.7 1.5,3.7H11.625L10.125,5.2H1.5V20.2H16.5V11.5L18,10V20.2C18,20.6 17.85,20.95 17.55,21.25C17.25,21.55 16.9,21.7 16.5,21.7H1.5ZM13.55,3.9L14.625,4.95L7.5,12.05V14.2H9.625L16.775,7.05L17.825,8.1L10.7,15.25C10.567,15.383 10.404,15.492 10.212,15.575C10.021,15.658 9.825,15.7 9.625,15.7H6.75C6.533,15.7 6.354,15.629 6.213,15.488C6.071,15.346 6,15.167 6,14.95V12.075C6,11.875 6.042,11.679 6.125,11.488C6.208,11.296 6.317,11.133 6.45,11L13.55,3.9ZM17.825,8.1L13.55,3.9L16.05,1.4C16.333,1.117 16.688,0.975 17.112,0.975C17.538,0.975 17.892,1.125 18.175,1.425L20.275,3.55C20.558,3.85 20.7,4.204 20.7,4.613C20.7,5.021 20.55,5.367 20.25,5.65L17.825,8.1Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M5,21.025C4.45,21.025 3.979,20.829 3.588,20.438C3.196,20.046 3,19.575 3,19.025V5.025C3,4.475 3.196,4.004 3.588,3.612C3.979,3.221 4.45,3.025 5,3.025H13.925L11.925,5.025H5V19.025H19V12.075L21,10.075V19.025C21,19.575 20.804,20.046 20.413,20.438C20.021,20.829 19.55,21.025 19,21.025H5ZM16.175,3.6L17.6,5L11,11.6V13.025H12.4L19.025,6.4L20.45,7.8L13.825,14.425C13.642,14.608 13.429,14.754 13.188,14.863C12.946,14.971 12.692,15.025 12.425,15.025H10C9.717,15.025 9.479,14.929 9.288,14.738C9.096,14.546 9,14.308 9,14.025V11.6C9,11.333 9.05,11.079 9.15,10.837C9.25,10.596 9.392,10.383 9.575,10.2L16.175,3.6ZM20.45,7.8L16.175,3.6L18.675,1.1C19.075,0.7 19.554,0.5 20.112,0.5C20.671,0.5 21.142,0.7 21.525,1.1L22.925,2.525C23.308,2.908 23.5,3.375 23.5,3.925C23.5,4.475 23.308,4.942 22.925,5.325L20.45,7.8Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -79,11 +79,11 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
suspend fun userAvatarUrl(userId: UserId): Result<String?>
|
||||
|
||||
suspend fun sendMessage(message: String): Result<Unit>
|
||||
suspend fun sendMessage(body: String, htmlBody: String): Result<Unit>
|
||||
|
||||
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, message: String): Result<Unit>
|
||||
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit>
|
||||
|
||||
suspend fun replyMessage(eventId: EventId, message: String): Result<Unit>
|
||||
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit>
|
||||
|
||||
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ import org.matrix.rustcomponents.sdk.RoomMember
|
|||
import org.matrix.rustcomponents.sdk.RoomSubscription
|
||||
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
|
|
@ -227,31 +227,32 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(message: String): Result<Unit> = withContext(roomDispatcher) {
|
||||
override suspend fun sendMessage(body: String, htmlBody: String): Result<Unit> = withContext(roomDispatcher) {
|
||||
val transactionId = genTransactionId()
|
||||
messageEventContentFromMarkdown(message).use { content ->
|
||||
messageEventContentFromHtml(body, htmlBody).use { content ->
|
||||
runCatching {
|
||||
innerRoom.send(content, transactionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, message: String): Result<Unit> = withContext(roomDispatcher) {
|
||||
if (originalEventId != null) {
|
||||
runCatching {
|
||||
innerRoom.edit(messageEventContentFromMarkdown(message), originalEventId.value, transactionId?.value)
|
||||
}
|
||||
} else {
|
||||
runCatching {
|
||||
transactionId?.let { cancelSend(it) }
|
||||
innerRoom.send(messageEventContentFromMarkdown(message), genTransactionId())
|
||||
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit> =
|
||||
withContext(roomDispatcher) {
|
||||
if (originalEventId != null) {
|
||||
runCatching {
|
||||
innerRoom.edit(messageEventContentFromHtml(body, htmlBody), originalEventId.value, transactionId?.value)
|
||||
}
|
||||
} else {
|
||||
runCatching {
|
||||
transactionId?.let { cancelSend(it) }
|
||||
innerRoom.send(messageEventContentFromHtml(body, htmlBody), genTransactionId())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun replyMessage(eventId: EventId, message: String): Result<Unit> = withContext(roomDispatcher) {
|
||||
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.sendReply(messageEventContentFromMarkdown(message), eventId.value, genTransactionId())
|
||||
innerRoom.sendReply(messageEventContentFromHtml(body, htmlBody), eventId.value, genTransactionId())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,25 +20,26 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.impl.room.RoomMemberMapper
|
||||
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
|
||||
class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) {
|
||||
|
||||
suspend fun create(roomListItem: RoomListItem, room: Room?): RoomSummaryDetails {
|
||||
val latestRoomMessage = roomListItem.latestEvent()?.use {
|
||||
fun create(roomInfo: RoomInfo): RoomSummaryDetails {
|
||||
val latestRoomMessage = roomInfo.latestEvent?.use {
|
||||
roomMessageFactory.create(it)
|
||||
}
|
||||
return RoomSummaryDetails(
|
||||
roomId = RoomId(roomListItem.id()),
|
||||
name = roomListItem.name() ?: roomListItem.id(),
|
||||
canonicalAlias = roomListItem.canonicalAlias(),
|
||||
isDirect = roomListItem.isDirect(),
|
||||
avatarURLString = roomListItem.avatarUrl(),
|
||||
unreadNotificationCount = roomListItem.unreadNotifications().use { it.notificationCount().toInt() },
|
||||
roomId = RoomId(roomInfo.id),
|
||||
name = roomInfo.name ?: roomInfo.id,
|
||||
canonicalAlias = roomInfo.canonicalAlias,
|
||||
isDirect = roomInfo.isDirect,
|
||||
avatarURLString = roomInfo.avatarUrl,
|
||||
unreadNotificationCount = roomInfo.notificationCount.toInt(),
|
||||
lastMessage = latestRoomMessage,
|
||||
lastMessageTimestamp = latestRoomMessage?.originServerTs,
|
||||
inviter = room?.inviter()?.let(RoomMemberMapper::map),
|
||||
inviter = roomInfo.inviter?.let(RoomMemberMapper::map),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,10 @@ import kotlinx.coroutines.CompletableDeferred
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
|
||||
import org.matrix.rustcomponents.sdk.RoomListEntry
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.RoomListService
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
|
|
@ -34,7 +33,6 @@ class RoomSummaryListProcessor(
|
|||
private val roomSummaries: MutableStateFlow<List<RoomSummary>>,
|
||||
private val roomListService: RoomListService,
|
||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
private val shouldFetchFullRoom: Boolean = false,
|
||||
) {
|
||||
|
||||
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
|
||||
|
|
@ -120,9 +118,9 @@ class RoomSummaryListProcessor(
|
|||
|
||||
private suspend fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
|
||||
val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem ->
|
||||
roomListItem.fullRoomOrNull().use { fullRoom ->
|
||||
roomListItem.roomInfo().use { roomInfo ->
|
||||
RoomSummary.Filled(
|
||||
details = roomSummaryDetailsFactory.create(roomListItem, fullRoom)
|
||||
details = roomSummaryDetailsFactory.create(roomInfo)
|
||||
)
|
||||
}
|
||||
} ?: buildEmptyRoomSummary()
|
||||
|
|
@ -130,14 +128,6 @@ class RoomSummaryListProcessor(
|
|||
return builtRoomSummary
|
||||
}
|
||||
|
||||
private fun RoomListItem.fullRoomOrNull(): Room? {
|
||||
return if (shouldFetchFullRoom) {
|
||||
fullRoom()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) =
|
||||
mutex.withLock {
|
||||
val mutableRoomSummaries = roomSummaries.value.toMutableList()
|
||||
|
|
|
|||
|
|
@ -53,9 +53,9 @@ class RustRoomListService(
|
|||
private val inviteRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
|
||||
|
||||
private val allRoomsLoadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
|
||||
private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = false)
|
||||
private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, roomSummaryDetailsFactory)
|
||||
private val invitesLoadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
|
||||
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, innerRoomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = true)
|
||||
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, innerRoomListService, roomSummaryDetailsFactory)
|
||||
|
||||
init {
|
||||
sessionCoroutineScope.launch(dispatcher) {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class FakeMatrixRoom(
|
|||
private var sendPollResponseResult = Result.success(Unit)
|
||||
private var endPollResult = Result.success(Unit)
|
||||
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
|
||||
val editMessageCalls = mutableListOf<String>()
|
||||
val editMessageCalls = mutableListOf<Pair<String, String>>()
|
||||
|
||||
var sendMediaCount = 0
|
||||
private set
|
||||
|
|
@ -171,7 +171,7 @@ class FakeMatrixRoom(
|
|||
userAvatarUrlResult
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(message: String): Result<Unit> = simulateLongTask {
|
||||
override suspend fun sendMessage(body: String, htmlBody: String) = simulateLongTask {
|
||||
Result.success(Unit)
|
||||
}
|
||||
|
||||
|
|
@ -200,16 +200,16 @@ class FakeMatrixRoom(
|
|||
return cancelSendResult
|
||||
}
|
||||
|
||||
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, message: String): Result<Unit> {
|
||||
editMessageCalls += message
|
||||
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit> {
|
||||
editMessageCalls += body to htmlBody
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
var replyMessageParameter: String? = null
|
||||
var replyMessageParameter: Pair<String, String>? = null
|
||||
private set
|
||||
|
||||
override suspend fun replyMessage(eventId: EventId, message: String): Result<Unit> {
|
||||
replyMessageParameter = message
|
||||
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit> {
|
||||
replyMessageParameter = body to htmlBody
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
|
|||
showDialog = showDialog.value,
|
||||
permissionAlreadyAsked = isAlreadyAsked,
|
||||
permissionAlreadyDenied = isAlreadyDenied,
|
||||
eventSink = ::handleEvents
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
* Copyright (c) 2023 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.
|
||||
|
|
@ -31,5 +31,9 @@ dependencies {
|
|||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
|
||||
implementation(libs.matrix.richtexteditor)
|
||||
api(libs.matrix.richtexteditor.compose)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.libraries.textcomposer
|
||||
|
||||
data class Message(
|
||||
val html: String,
|
||||
val markdown: String,
|
||||
)
|
||||
|
|
@ -37,38 +37,25 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -88,23 +75,23 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
|||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.wysiwyg.compose.RichTextEditor
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorDefaults
|
||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||
import kotlinx.coroutines.android.awaitFrame
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun TextComposer(
|
||||
composerText: String?,
|
||||
state: RichTextEditorState,
|
||||
composerMode: MessageComposerMode,
|
||||
composerCanSendMessage: Boolean,
|
||||
canSendMessage: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
focusRequester: FocusRequester = FocusRequester(),
|
||||
onSendMessage: (String) -> Unit = {},
|
||||
onRequestFocus: () -> Unit = {},
|
||||
onSendMessage: (Message) -> Unit = {},
|
||||
onResetComposerMode: () -> Unit = {},
|
||||
onComposerTextChange: (String) -> Unit = {},
|
||||
onAddAttachment: () -> Unit = {},
|
||||
onFocusChanged: (Boolean) -> Unit = {},
|
||||
onError: (Throwable) -> Unit = {},
|
||||
) {
|
||||
val text = composerText.orEmpty()
|
||||
Row(
|
||||
modifier.padding(
|
||||
horizontal = 12.dp,
|
||||
|
|
@ -115,10 +102,9 @@ fun TextComposer(
|
|||
Spacer(modifier = Modifier.width(12.dp))
|
||||
val roundCornerSmall = 20.dp.applyScaleUp()
|
||||
val roundCornerLarge = 28.dp.applyScaleUp()
|
||||
var lineCount by remember { mutableIntStateOf(0) }
|
||||
|
||||
val roundedCornerSize = remember(lineCount, composerMode) {
|
||||
if (lineCount > 1 || composerMode is MessageComposerMode.Special) {
|
||||
val roundedCornerSize = remember(state.lineCount, composerMode) {
|
||||
if (state.lineCount > 1 || composerMode is MessageComposerMode.Special) {
|
||||
roundCornerSmall
|
||||
} else {
|
||||
roundCornerLarge
|
||||
|
|
@ -132,10 +118,15 @@ fun TextComposer(
|
|||
)
|
||||
val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value)
|
||||
val minHeight = 42.dp.applyScaleUp()
|
||||
val bgColor = ElementTheme.colors.bgSubtleSecondary
|
||||
// Change border color depending on focus
|
||||
var hasFocus by remember { mutableStateOf(false) }
|
||||
val borderColor = if (hasFocus) ElementTheme.colors.borderDisabled else bgColor
|
||||
val colors = ElementTheme.colors
|
||||
val bgColor = colors.bgSubtleSecondary
|
||||
|
||||
val borderColor by remember(state.hasFocus, colors) {
|
||||
derivedStateOf {
|
||||
if (state.hasFocus) colors.borderDisabled else bgColor
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -147,66 +138,56 @@ fun TextComposer(
|
|||
ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode)
|
||||
}
|
||||
val defaultTypography = ElementTheme.typography.fontBodyLgRegular
|
||||
|
||||
Box {
|
||||
BasicTextField(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeight)
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusEvent {
|
||||
hasFocus = it.hasFocus
|
||||
onFocusChanged(it.hasFocus)
|
||||
},
|
||||
value = text,
|
||||
onValueChange = { onComposerTextChange(it) },
|
||||
onTextLayout = {
|
||||
lineCount = it.lineCount
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
textStyle = defaultTypography.copy(color = MaterialTheme.colorScheme.primary),
|
||||
cursorBrush = SolidColor(ElementTheme.colors.iconAccentTertiary),
|
||||
decorationBox = { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = text,
|
||||
innerTextField = innerTextField,
|
||||
enabled = true,
|
||||
singleLine = false,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
shape = roundedCorners,
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp.applyScaleUp(),
|
||||
bottom = 10.dp.applyScaleUp(),
|
||||
.background(color = bgColor, shape = roundedCorners)
|
||||
.padding(
|
||||
PaddingValues(
|
||||
top = 4.dp.applyScaleUp(),
|
||||
bottom = 4.dp.applyScaleUp(),
|
||||
start = 12.dp.applyScaleUp(),
|
||||
end = 42.dp.applyScaleUp(),
|
||||
),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
placeholder = {
|
||||
Text(stringResource(CommonStrings.common_message), style = defaultTypography)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
unfocusedTextColor = MaterialTheme.colorScheme.secondary,
|
||||
focusedTextColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedPlaceholderColor = ElementTheme.colors.textDisabled,
|
||||
focusedPlaceholderColor = ElementTheme.colors.textDisabled,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
errorIndicatorColor = Color.Transparent,
|
||||
unfocusedContainerColor = bgColor,
|
||||
focusedContainerColor = bgColor,
|
||||
errorContainerColor = bgColor,
|
||||
disabledContainerColor = bgColor,
|
||||
end = 42.dp.applyScaleUp()
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
|
||||
// Placeholder
|
||||
if (state.messageHtml.isEmpty()) {
|
||||
Text(
|
||||
stringResource(CommonStrings.common_message),
|
||||
style = defaultTypography.copy(
|
||||
color = ElementTheme.colors.textDisabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
RichTextEditor(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
style = RichTextEditorDefaults.style(
|
||||
text = RichTextEditorDefaults.textStyle(
|
||||
color = if (state.hasFocus) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondary
|
||||
}
|
||||
),
|
||||
cursor = RichTextEditorDefaults.cursorStyle(
|
||||
color = ElementTheme.colors.iconAccentTertiary,
|
||||
)
|
||||
),
|
||||
onError = onError
|
||||
)
|
||||
}
|
||||
|
||||
SendButton(
|
||||
text = text,
|
||||
canSendMessage = composerCanSendMessage,
|
||||
onSendMessage = onSendMessage,
|
||||
canSendMessage = canSendMessage,
|
||||
onClick = { onSendMessage(Message(html = state.messageHtml, markdown = state.messageMarkdown)) },
|
||||
composerMode = composerMode,
|
||||
modifier = Modifier.padding(end = 6.dp.applyScaleUp(), bottom = 6.dp.applyScaleUp())
|
||||
)
|
||||
|
|
@ -218,7 +199,7 @@ fun TextComposer(
|
|||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(composerMode) {
|
||||
if (composerMode is MessageComposerMode.Special) {
|
||||
focusRequester.requestFocus()
|
||||
onRequestFocus()
|
||||
keyboard?.let {
|
||||
awaitFrame()
|
||||
it.show()
|
||||
|
|
@ -241,7 +222,7 @@ private fun ComposerModeView(
|
|||
ReplyToModeView(
|
||||
modifier = modifier.padding(8.dp),
|
||||
senderName = composerMode.senderName,
|
||||
text = composerMode.defaultContent.toString(),
|
||||
text = composerMode.defaultContent,
|
||||
attachmentThumbnailInfo = composerMode.attachmentThumbnailInfo,
|
||||
onResetComposerMode = onResetComposerMode,
|
||||
)
|
||||
|
|
@ -385,9 +366,8 @@ private fun AttachmentButton(
|
|||
|
||||
@Composable
|
||||
private fun BoxScope.SendButton(
|
||||
text: String,
|
||||
canSendMessage: Boolean,
|
||||
onSendMessage: (String) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
composerMode: MessageComposerMode,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -405,9 +385,8 @@ private fun BoxScope.SendButton(
|
|||
enabled = canSendMessage,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(bounded = false),
|
||||
onClick = {
|
||||
onSendMessage(text)
|
||||
}),
|
||||
onClick = onClick,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val iconId = when (composerMode) {
|
||||
|
|
@ -433,28 +412,37 @@ private fun BoxScope.SendButton(
|
|||
internal fun TextComposerSimplePreview() = ElementPreview {
|
||||
Column {
|
||||
TextComposer(
|
||||
RichTextEditorState("", fake = true).apply { requestFocus() },
|
||||
canSendMessage = false,
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Normal(""),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = false,
|
||||
composerText = "",
|
||||
)
|
||||
TextComposer(
|
||||
RichTextEditorState("A message", fake = true).apply { requestFocus() },
|
||||
canSendMessage = true,
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Normal(""),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message",
|
||||
)
|
||||
TextComposer(
|
||||
RichTextEditorState(
|
||||
"A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
|
||||
fake = true
|
||||
).apply {
|
||||
requestFocus()
|
||||
},
|
||||
canSendMessage = true,
|
||||
onSendMessage = {},
|
||||
composerMode = MessageComposerMode.Normal(""),
|
||||
onResetComposerMode = {},
|
||||
)
|
||||
TextComposer(
|
||||
RichTextEditorState("A message without focus", fake = true),
|
||||
canSendMessage = true,
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Normal(""),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -463,12 +451,11 @@ internal fun TextComposerSimplePreview() = ElementPreview {
|
|||
@Composable
|
||||
internal fun TextComposerEditPreview() = ElementPreview {
|
||||
TextComposer(
|
||||
RichTextEditorState("A message", fake = true).apply { requestFocus() },
|
||||
canSendMessage = true,
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -477,8 +464,9 @@ internal fun TextComposerEditPreview() = ElementPreview {
|
|||
internal fun TextComposerReplyPreview() = ElementPreview {
|
||||
Column {
|
||||
TextComposer(
|
||||
RichTextEditorState("", fake = true),
|
||||
canSendMessage = false,
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
senderName = "Alice",
|
||||
eventId = EventId("$1234"),
|
||||
|
|
@ -488,12 +476,11 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
"To preview larger textfields and long lines with overflow"
|
||||
),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message",
|
||||
)
|
||||
TextComposer(
|
||||
RichTextEditorState("A message", fake = true),
|
||||
canSendMessage = true,
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
senderName = "Alice",
|
||||
eventId = EventId("$1234"),
|
||||
|
|
@ -506,12 +493,11 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
defaultContent = "image.jpg"
|
||||
),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message",
|
||||
)
|
||||
TextComposer(
|
||||
RichTextEditorState("A message", fake = true),
|
||||
canSendMessage = true,
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
senderName = "Alice",
|
||||
eventId = EventId("$1234"),
|
||||
|
|
@ -524,12 +510,11 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
defaultContent = "video.mp4"
|
||||
),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message",
|
||||
)
|
||||
TextComposer(
|
||||
RichTextEditorState("A message", fake = true),
|
||||
canSendMessage = true,
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
senderName = "Alice",
|
||||
eventId = EventId("$1234"),
|
||||
|
|
@ -542,12 +527,11 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
defaultContent = "logs.txt"
|
||||
),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message",
|
||||
)
|
||||
TextComposer(
|
||||
RichTextEditorState("A message", fake = true).apply { requestFocus() },
|
||||
canSendMessage = true,
|
||||
onSendMessage = {},
|
||||
onComposerTextChange = {},
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
senderName = "Alice",
|
||||
eventId = EventId("$1234"),
|
||||
|
|
@ -560,8 +544,6 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
defaultContent = "Shared location"
|
||||
),
|
||||
onResetComposerMode = {},
|
||||
composerCanSendMessage = true,
|
||||
composerText = "A message",
|
||||
)
|
||||
}
|
||||
}
|
||||
28
libraries/textcomposer/test/build.gradle.kts
Normal file
28
libraries/textcomposer/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.textcomposer.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.textcomposer.impl)
|
||||
implementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -99,7 +99,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
|||
implementation(project(":libraries:mediapickers:impl"))
|
||||
implementation(project(":libraries:mediaupload:impl"))
|
||||
implementation(project(":libraries:usersearch:impl"))
|
||||
implementation(project(":libraries:textcomposer"))
|
||||
implementation(project(":libraries:textcomposer:impl"))
|
||||
}
|
||||
|
||||
fun DependencyHandlerScope.allServicesImpl() {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class FakeAnalyticsService(
|
|||
private val isEnabledFlow = MutableStateFlow(isEnabled)
|
||||
private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
||||
val capturedEvents = mutableListOf<VectorAnalyticsEvent>()
|
||||
val trackedErrors = mutableListOf<Throwable>()
|
||||
|
||||
override fun getAvailableAnalyticsProviders(): Set<AnalyticsProvider> = emptySet()
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ class FakeAnalyticsService(
|
|||
}
|
||||
|
||||
override fun trackError(throwable: Throwable) {
|
||||
trackedErrors += throwable
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1625b6184c01ab75efcfe4c8d1594af1ffb84f0416219580adce5a9371fabe7d
|
||||
size 21292
|
||||
oid sha256:ad8519086ae725ac7b1aeae9cd628237afeb489c6000c814b4d8e3f644592e58
|
||||
size 22278
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:91d1b5d5c58a4f2cbf048ad20b3a9742c209ea19703eafaf6d3e6f542caf2111
|
||||
size 40640
|
||||
oid sha256:f4cc5cd0ba741aa217ee000749ec4041186c2c83d13f6c87023d16f16689a9ed
|
||||
size 41359
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fe37eb6328241c70b4219c9fde9118593b1354fe99628e106af828868f9cc4c6
|
||||
size 39178
|
||||
oid sha256:1ff04ca56c00e2bbf4858bcf6f8faed596e2c538937ec26d61abac7922cda2e1
|
||||
size 39890
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1625b6184c01ab75efcfe4c8d1594af1ffb84f0416219580adce5a9371fabe7d
|
||||
size 21292
|
||||
oid sha256:ad8519086ae725ac7b1aeae9cd628237afeb489c6000c814b4d8e3f644592e58
|
||||
size 22278
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c94b320a8a6e1bbf2824eaa66d80476a293405b6bd0e7c2c96ce3b016be8f291
|
||||
size 21430
|
||||
oid sha256:b461bb08151c6efb41b0c4d7a40c33b3710dc5a1d0a32cb707ba90f0ef1ee2d1
|
||||
size 22419
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eba27416d7da08fbd8c47a172f3b54a603c893e1d07faff443417e4949fbc985
|
||||
size 19849
|
||||
oid sha256:0a59281bb44aea2e220f48f1229afd6b4d2d0551dc0a7dd6e19b7a6ff3984f09
|
||||
size 20742
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:40184d796909922b4316ee027e4500fa183d275bc73d7f69e51801e3f956333b
|
||||
size 37569
|
||||
oid sha256:ac124e959b3c2303ecabe6eeba7c4faf7de55929757c0b69ceb4c0021cc1a390
|
||||
size 38197
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5647bd3d7605f49358a4e1a76bf5fa86ab9a3ad32d180a134ea5e5271d8d8093
|
||||
size 36026
|
||||
oid sha256:7e064103aa27ef0b2765962bb5d21f24387ac215d27691570991a75dd85f8725
|
||||
size 36633
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eba27416d7da08fbd8c47a172f3b54a603c893e1d07faff443417e4949fbc985
|
||||
size 19849
|
||||
oid sha256:0a59281bb44aea2e220f48f1229afd6b4d2d0551dc0a7dd6e19b7a6ff3984f09
|
||||
size 20742
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:434170541079e63b9c606e592407168f8e7e1e695f5f3e188428570ec364e2c6
|
||||
size 20047
|
||||
oid sha256:5b7db80ee7f9c4a4d74b0665803c1cf85a391b6af0643f7cc823de5e8897ba6e
|
||||
size 20904
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d502b465f6f70ead5b9bc373b443d548053bc1d0867b4cbfb52d84e59fa253f1
|
||||
size 11596
|
||||
oid sha256:905d574bc64f14e309072a508732281c87dd00f8fa756bbd20b6d59c9b50eee6
|
||||
size 12428
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:81c29f9bb336e74a97189d8361490a3a29c4f4678d80f62c2e384396cb90a871
|
||||
size 32465
|
||||
oid sha256:1cb7669bfe98a30d0658c4444f08c11de6b8589a8a81163cb9a83366343c3aea
|
||||
size 33113
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:daf1ab743402e503ff6a5d0ec5f7a599b531c23fcbd9767570527828530bf5b3
|
||||
size 30817
|
||||
oid sha256:520a4053273d7d72a7339fdc609355ffbca183cbf151a24c701b9674e806306d
|
||||
size 31453
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d502b465f6f70ead5b9bc373b443d548053bc1d0867b4cbfb52d84e59fa253f1
|
||||
size 11596
|
||||
oid sha256:905d574bc64f14e309072a508732281c87dd00f8fa756bbd20b6d59c9b50eee6
|
||||
size 12428
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:03769782960e9fc1abf6c609a318ca90debb03484d7b8d61d0703ef1374a0b55
|
||||
size 11786
|
||||
oid sha256:71e4415423627091abedfccd7a03e20afa27fc9f599de70dd82be1571c4131a0
|
||||
size 12616
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e0c113e7eb9d495ab2fa3d03a945fd6fac35810af2df4571703a8a3f2b55a079
|
||||
size 15018
|
||||
oid sha256:ca76be1bf0a76e93614c5bca704269932d690f6133f53d09275e2f08d3d27200
|
||||
size 15843
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:92da6c43ebe2da8ad035e0f03ddd5261491e846212396240aee8287d4f4d9a10
|
||||
size 22631
|
||||
oid sha256:4d0e5e459ed40f12fa3d2897a53928bc6808903bc8a36938e7954be41eab80b2
|
||||
size 23415
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c23fded123579979a9b6acf41def9a864ff99de6adbf224014c6d77a552fb2fe
|
||||
size 24754
|
||||
oid sha256:adb557b5ccc00a8bbb5b605e372c892f6c0b7d194ed3e6e352f05b975e2e0eab
|
||||
size 25525
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6519ea26cb85aa1cecdab38d9f3cd54f3e986d33a9d411c576714fb070835d6f
|
||||
size 12503
|
||||
oid sha256:c581e4cf30caec4e5039f11091417780ab0decf779ec7b31112a94fec0a596e1
|
||||
size 13524
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1cdb9c379d685e7664c76f4ae7c712665de8dc8ad1f65441121dde4475a92f57
|
||||
size 35269
|
||||
oid sha256:e82b0029ac4e23aacfde992e36267eec369108d6ef9021138510d4b421017bd3
|
||||
size 36033
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0c5285cb82229e84a7cf2c1f2ce375060ec4f99577c88a9840b6db3b8d0966cb
|
||||
size 33736
|
||||
oid sha256:9229d55e1a109abacfa82f066a48fa60606a0d0ed0d155634b4c31fb2c7be98c
|
||||
size 34501
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6519ea26cb85aa1cecdab38d9f3cd54f3e986d33a9d411c576714fb070835d6f
|
||||
size 12503
|
||||
oid sha256:c581e4cf30caec4e5039f11091417780ab0decf779ec7b31112a94fec0a596e1
|
||||
size 13524
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d33eeef612e03599f835991ad70c5f128c51e0ac49e67fff445b2e98ed3b71ef
|
||||
size 12706
|
||||
oid sha256:9703d279e6401268fd066984d4cc91403c7f8f8e2c248c6ba2ea4c242694170f
|
||||
size 13729
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b47af4d036f0cf1275773fb8b81d3cae847626b49f5197e2249df1d9d9be2cbb
|
||||
size 16501
|
||||
oid sha256:11a6fd8151812c8733bab26904f53754899db2caeaa0f4f24ea010dd2d54b538
|
||||
size 17522
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1aca2f20b99dbbdbd70ca437c06949b634d10c30701f7634cfebaf78593c34d8
|
||||
size 25515
|
||||
oid sha256:0083578f572e51de0a0464caf90ec68e8a82930b97e0217d20a550ce269f7519
|
||||
size 26504
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f612c17dedc33b8c4089828673dd58d6f1b51faa0c6df3edb1a0f3398c4c55e2
|
||||
size 27885
|
||||
oid sha256:1a5e11eed79ab3f92c130c3055e70e5cf0df2b22200e8e73e2180c0114ff5b04
|
||||
size 28881
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:906c643393af8d290f0635d7560eaa54339fc0498744f9ab8139932986d73a8c
|
||||
size 9740
|
||||
oid sha256:4013454094701004f3c132df1ea1c3db2aa56a025b9edea80417fdd16fe55b19
|
||||
size 10422
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bd57f885f161316550712dcb471ed213537a395539ed5b9975e17465310803b7
|
||||
size 9823
|
||||
oid sha256:db74b1655e377e4fd8c35aa18c22ef5f5641586b64d5e582ab2079afeac4b8c3
|
||||
size 10775
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:39b527075df2b3f7afa4f3ecfca1ac93a180855b20d2bd64cb763fb98b0a7b69
|
||||
size 51340
|
||||
oid sha256:cfc6f54988e4fab08ff443f0c2ea285d47e3d030a8e6c92a1294ee0f85dc1269
|
||||
size 51967
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:81f196dc08f4de31f4257f6ba89c52ea41a97b107e7c5c04261188e6c572d4d0
|
||||
size 52594
|
||||
oid sha256:d2ffd4fc93e7da38836fd860ce5e505c5f0c25fdd6558efef480aaeb05d00dd5
|
||||
size 53327
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a648d286a3b0536fc97801c3281bb8df432c7ffa28efff66f67cd6cf4c546c10
|
||||
size 51579
|
||||
oid sha256:96864552336c65b464251c262f330856fc82c69a5f92f89b1357a185d654fccd
|
||||
size 52252
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:34f9eacfd61965cfde4f420fe6a0fc0d637a21aca0d4b12c6a6f1642bd44556d
|
||||
size 50586
|
||||
oid sha256:2411212e87f95415e0b4ed6732387922faed891cde5e78fa9f0691a185873129
|
||||
size 51026
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c336a9cbc803eb314aa9333719d813ae64994a75344aa363b25aee8d52551b21
|
||||
size 48928
|
||||
oid sha256:255560f478c093f4ca7d021646f4cf9064c16b420d5eeec7cbce97d26c751d92
|
||||
size 49540
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5d7148068516b5e3299cce2924ea3cb54c8f1ddd6378e48b0fa746217a547d22
|
||||
size 52940
|
||||
oid sha256:a384aa84ae514aa4989d4b4a5ef8048cb8810f1f37056137c77fd0ee798a0bab
|
||||
size 53941
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b8aaf2c70473711a24bf587ff524fd71d43c2610ade1c3d4ff2013705d5670b3
|
||||
size 54314
|
||||
oid sha256:a48badf15c59ab5c42fc7cc200971f00cefa7838461ad0cb59537c2b4065bfff
|
||||
size 55409
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:566d5b197091d4b740e1241eab54017fd31e65f570def4b8c24ade19ea78ccc8
|
||||
size 53254
|
||||
oid sha256:88a498b9070653513b3e27aa2b597367ee85c8526c6e4c3712a4cb0c06e5313b
|
||||
size 54353
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8353077959916fa85046d3a81d1479d552748635f1286bb8bacc6d922ba30f28
|
||||
size 55312
|
||||
oid sha256:bf493a9cb7325b4f68924abd86864b37c72226afcf1c4179a06c17e923cfd0ae
|
||||
size 55837
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a84dabfdcbaff8de5c30472676dee2cee8dc0f77469f2802dfc796a87bc10dff
|
||||
size 50328
|
||||
oid sha256:8ac3dbf36380fa273d82ebe8836c525bbeee004d6348822d965b216f9bcdb920
|
||||
size 51335
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e045c8686effc435e5d31b7ef034d93bca64a226d2e6ebae941afeb36dbad099
|
||||
size 35534
|
||||
oid sha256:4482b15d928567822dc984a1f19c698f6a9d981d907b70635b3054795292306a
|
||||
size 36543
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a62f8d9a50f7d23bc355dfe98be0bc5eeb2b68866e3035b79b51591eae2f781f
|
||||
size 58626
|
||||
oid sha256:8b22df92cc4ec3e9622f23f6fb7e800e17afbe3a12f9f3d4121b1981ac0350f8
|
||||
size 59635
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e045c8686effc435e5d31b7ef034d93bca64a226d2e6ebae941afeb36dbad099
|
||||
size 35534
|
||||
oid sha256:4482b15d928567822dc984a1f19c698f6a9d981d907b70635b3054795292306a
|
||||
size 36543
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:76b43763ca93e7bf31e49aa4ba98e9c9cb6d5ee2ab6dba573e69315084884859
|
||||
size 37433
|
||||
oid sha256:c78072a5ab2e95aaf7abb085db0d66761eb987b22c0e1dcd6da901f3fbe2a3d7
|
||||
size 38438
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0451e8d5939e9c90ccef22a8384d80e7d83cf8faa8a7df4bcd6032d9f3ef394c
|
||||
size 36884
|
||||
oid sha256:2cbf0a4e9ca5d3471d361890737d5aa9636abe3143f5de9218e0568951d59bf5
|
||||
size 37887
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9220e4a8dca8924d574e9a5511f25f6c41d14163b58cb4d8f622d7fd8fc71ae
|
||||
size 37238
|
||||
oid sha256:12cac3731fd61b4cbfec09b0f439b2744a5374b6d9ee2b79880507af2224b969
|
||||
size 38241
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d24c1170affdcae6f189ba2e6646fb61e01f8eed4d83340297f3f189c2f3470
|
||||
size 38225
|
||||
oid sha256:b03c0240edcc58f64bd1dc7e9ef619081f4019c5825c6e4133f3aab4b7cf9b7a
|
||||
size 39711
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1cca9ca327d8a64cb04489cd52c52d3871bd583de9835eb266a1e60db8e53f84
|
||||
size 62191
|
||||
oid sha256:f2ca4bee47e6beaf4b79c0417dbfe132f6e92974c33484bb0440929a35ada794
|
||||
size 63618
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d24c1170affdcae6f189ba2e6646fb61e01f8eed4d83340297f3f189c2f3470
|
||||
size 38225
|
||||
oid sha256:b03c0240edcc58f64bd1dc7e9ef619081f4019c5825c6e4133f3aab4b7cf9b7a
|
||||
size 39711
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fcfec887ae55ce77b049ef49880f1858e7a0a0ea2a7c81140c8c66069a598a14
|
||||
size 40265
|
||||
oid sha256:19e78d44954b0cde8ea83873f40e6b0a5cf53a02b4bd523e8b8710a483471e32
|
||||
size 41698
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5716646f8c5d8e9488f79b033ffce5f337133eb78a63328ec616b8eb35d1c70b
|
||||
size 39534
|
||||
oid sha256:6ff479b11d48f3bdf5adc02d466b6427cbf778b2d2184d6e88d7abe8c12043c5
|
||||
size 40960
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4a8723f12c2884d20c195a779cf6d48a08191f1caa0acdaa2324e478ecac92b2
|
||||
size 39898
|
||||
oid sha256:1f9e4cc9f63f4414fbe1f82e82b9eca7b5071e26aa057b1137daa75cecccf150
|
||||
size 41329
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue