Merge branch 'develop' into feature/fga/timeline_thread_decoration
This commit is contained in:
commit
f27f49160d
124 changed files with 566 additions and 304 deletions
1
changelog.d/1173.bugfix
Normal file
1
changelog.d/1173.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Reply action: harmonize conditions in bottom sheet and swipe to reply.
|
||||||
1
changelog.d/1289.feature
Normal file
1
changelog.d/1289.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[Rich text editor] Add feature flag for rich text editor. Markdown support can now be enabled by disabling the rich text editor.
|
||||||
|
|
@ -26,10 +26,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.location.impl.common.MapDefaults
|
import io.element.android.features.location.impl.common.MapDefaults
|
||||||
|
import io.element.android.features.location.impl.common.actions.LocationActions
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||||
import io.element.android.features.location.impl.common.actions.LocationActions
|
|
||||||
import io.element.android.features.messages.api.MessageComposerContext
|
import io.element.android.features.messages.api.MessageComposerContext
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
|
@ -119,9 +119,8 @@ class SendLocationPresenter @Inject constructor(
|
||||||
Composer(
|
Composer(
|
||||||
inThread = messageComposerContext.composerMode.inThread,
|
inThread = messageComposerContext.composerMode.inThread,
|
||||||
isEditing = messageComposerContext.composerMode.isEditing,
|
isEditing = messageComposerContext.composerMode.isEditing,
|
||||||
isLocation = true,
|
|
||||||
isReply = messageComposerContext.composerMode.isReply,
|
isReply = messageComposerContext.composerMode.isReply,
|
||||||
locationType = Composer.LocationType.PinDrop,
|
messageType = Composer.MessageType.LocationPin,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -138,9 +137,8 @@ class SendLocationPresenter @Inject constructor(
|
||||||
Composer(
|
Composer(
|
||||||
inThread = messageComposerContext.composerMode.inThread,
|
inThread = messageComposerContext.composerMode.inThread,
|
||||||
isEditing = messageComposerContext.composerMode.isEditing,
|
isEditing = messageComposerContext.composerMode.isEditing,
|
||||||
isLocation = true,
|
|
||||||
isReply = messageComposerContext.composerMode.isReply,
|
isReply = messageComposerContext.composerMode.isReply,
|
||||||
locationType = Composer.LocationType.MyLocation,
|
messageType = Composer.MessageType.LocationUser,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -308,9 +308,8 @@ class SendLocationPresenterTest {
|
||||||
Composer(
|
Composer(
|
||||||
inThread = false,
|
inThread = false,
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
isLocation = true,
|
|
||||||
isReply = false,
|
isReply = false,
|
||||||
locationType = Composer.LocationType.MyLocation,
|
messageType = Composer.MessageType.LocationUser,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -365,9 +364,8 @@ class SendLocationPresenterTest {
|
||||||
Composer(
|
Composer(
|
||||||
inThread = false,
|
inThread = false,
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
isLocation = true,
|
|
||||||
isReply = false,
|
isReply = false,
|
||||||
locationType = Composer.LocationType.PinDrop,
|
messageType = Composer.MessageType.LocationPin,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -412,9 +410,8 @@ class SendLocationPresenterTest {
|
||||||
Composer(
|
Composer(
|
||||||
inThread = false,
|
inThread = false,
|
||||||
isEditing = true,
|
isEditing = true,
|
||||||
isLocation = true,
|
|
||||||
isReply = false,
|
isReply = false,
|
||||||
locationType = Composer.LocationType.PinDrop,
|
messageType = Composer.MessageType.LocationPin,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.features.analytics.plan.PollEnd
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
|
|
@ -65,6 +66,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||||
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
|
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
|
|
@ -74,6 +77,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
import io.element.android.libraries.matrix.ui.room.canRedactAsState
|
import io.element.android.libraries.matrix.ui.room.canRedactAsState
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -92,6 +96,8 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val messageSummaryFormatter: MessageSummaryFormatter,
|
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val clipboardHelper: ClipboardHelper,
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
@Assisted private val navigator: MessagesNavigator,
|
@Assisted private val navigator: MessagesNavigator,
|
||||||
) : Presenter<MessagesState> {
|
) : Presenter<MessagesState> {
|
||||||
|
|
||||||
|
|
@ -140,6 +146,11 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
|
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var enableTextFormatting by remember { mutableStateOf(true) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
enableTextFormatting = featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(event: MessagesEvents) {
|
fun handleEvents(event: MessagesEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is MessagesEvents.HandleAction -> {
|
is MessagesEvents.HandleAction -> {
|
||||||
|
|
@ -175,6 +186,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
showReinvitePrompt = showReinvitePrompt,
|
showReinvitePrompt = showReinvitePrompt,
|
||||||
inviteProgress = inviteProgress.value,
|
inviteProgress = inviteProgress.value,
|
||||||
|
enableTextFormatting = enableTextFormatting,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -247,11 +259,15 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
private suspend fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
||||||
val composerMode = MessageComposerMode.Edit(
|
val composerMode = MessageComposerMode.Edit(
|
||||||
targetEvent.eventId,
|
targetEvent.eventId,
|
||||||
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
|
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
|
||||||
it.htmlBody ?: it.body
|
if (featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)) {
|
||||||
|
it.htmlBody ?: it.body
|
||||||
|
} else {
|
||||||
|
it.body
|
||||||
|
}
|
||||||
}.orEmpty(),
|
}.orEmpty(),
|
||||||
targetEvent.transactionId,
|
targetEvent.transactionId,
|
||||||
)
|
)
|
||||||
|
|
@ -321,8 +337,10 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleEndPollAction(event: TimelineItem.Event) {
|
private suspend fun handleEndPollAction(event: TimelineItem.Event) {
|
||||||
event.eventId?.let { room.endPoll(it, "The poll with event id: $it has ended.") }
|
event.eventId?.let {
|
||||||
// TODO Polls: Send poll end analytic
|
room.endPoll(it, "The poll with event id: $it has ended.")
|
||||||
|
analyticsService.capture(PollEnd())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleCopyContents(event: TimelineItem.Event) {
|
private suspend fun handleCopyContents(event: TimelineItem.Event) {
|
||||||
|
|
|
||||||
|
|
@ -45,5 +45,6 @@ data class MessagesState(
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val inviteProgress: Async<Unit>,
|
val inviteProgress: Async<Unit>,
|
||||||
val showReinvitePrompt: Boolean,
|
val showReinvitePrompt: Boolean,
|
||||||
|
val enableTextFormatting: Boolean,
|
||||||
val eventSink: (MessagesEvents) -> Unit
|
val eventSink: (MessagesEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -82,5 +82,6 @@ fun aMessagesState() = MessagesState(
|
||||||
snackbarMessage = null,
|
snackbarMessage = null,
|
||||||
inviteProgress = Async.Uninitialized,
|
inviteProgress = Async.Uninitialized,
|
||||||
showReinvitePrompt = false,
|
showReinvitePrompt = false,
|
||||||
|
enableTextFormatting = true,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,13 @@ fun MessagesView(
|
||||||
fun onMessageLongClicked(event: TimelineItem.Event) {
|
fun onMessageLongClicked(event: TimelineItem.Event) {
|
||||||
Timber.v("OnMessageLongClicked= ${event.id}")
|
Timber.v("OnMessageLongClicked= ${event.id}")
|
||||||
localView.hideKeyboard()
|
localView.hideKeyboard()
|
||||||
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event, state.userHasPermissionToRedact))
|
state.actionListState.eventSink(
|
||||||
|
ActionListEvents.ComputeForMessage(
|
||||||
|
event = event,
|
||||||
|
canRedact = state.userHasPermissionToRedact,
|
||||||
|
canSendMessage = state.userHasPermissionToSendMessage,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
|
||||||
|
|
@ -203,8 +209,8 @@ fun MessagesView(
|
||||||
CustomReactionBottomSheet(
|
CustomReactionBottomSheet(
|
||||||
state = state.customReactionState,
|
state = state.customReactionState,
|
||||||
onEmojiSelected = { eventId, emoji ->
|
onEmojiSelected = { eventId, emoji ->
|
||||||
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
|
||||||
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -298,6 +304,7 @@ private fun MessagesViewContent(
|
||||||
state = state.composerState,
|
state = state.composerState,
|
||||||
onSendLocationClicked = onSendLocationClicked,
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
onCreatePollClicked = onCreatePollClicked,
|
onCreatePollClicked = onCreatePollClicked,
|
||||||
|
enableTextFormatting = state.enableTextFormatting,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight(Alignment.Bottom)
|
.wrapContentHeight(Alignment.Bottom)
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
sealed interface ActionListEvents {
|
sealed interface ActionListEvents {
|
||||||
data object Clear : ActionListEvents
|
data object Clear : ActionListEvents
|
||||||
data class ComputeForMessage(val event: TimelineItem.Event, val canRedact: Boolean) : ActionListEvents
|
data class ComputeForMessage(
|
||||||
|
val event: TimelineItem.Event,
|
||||||
|
val canRedact: Boolean,
|
||||||
|
val canSendMessage: Boolean,
|
||||||
|
) : ActionListEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
|
||||||
timelineItem = event.event,
|
timelineItem = event.event,
|
||||||
userCanRedact = event.canRedact,
|
userCanRedact = event.canRedact,
|
||||||
|
userCanSendMessage = event.canSendMessage,
|
||||||
target = target,
|
target = target,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -77,6 +78,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
private fun CoroutineScope.computeForMessage(
|
private fun CoroutineScope.computeForMessage(
|
||||||
timelineItem: TimelineItem.Event,
|
timelineItem: TimelineItem.Event,
|
||||||
userCanRedact: Boolean,
|
userCanRedact: Boolean,
|
||||||
|
userCanSendMessage: Boolean,
|
||||||
target: MutableState<ActionListState.Target>
|
target: MutableState<ActionListState.Target>
|
||||||
) = launch {
|
) = launch {
|
||||||
target.value = ActionListState.Target.Loading(timelineItem)
|
target.value = ActionListState.Target.Loading(timelineItem)
|
||||||
|
|
@ -101,7 +103,8 @@ class ActionListPresenter @Inject constructor(
|
||||||
buildList {
|
buildList {
|
||||||
val isMineOrCanRedact = timelineItem.isMine || userCanRedact
|
val isMineOrCanRedact = timelineItem.isMine || userCanRedact
|
||||||
|
|
||||||
// TODO Poll: Reply to poll
|
// TODO Poll: Reply to poll. Ensure to update `fun TimelineItemEventContent.canBeReplied()`
|
||||||
|
// when touching this
|
||||||
// if (timelineItem.isRemote) {
|
// if (timelineItem.isRemote) {
|
||||||
// // Can only reply or forward messages already uploaded to the server
|
// // Can only reply or forward messages already uploaded to the server
|
||||||
// add(TimelineItemAction.Reply)
|
// add(TimelineItemAction.Reply)
|
||||||
|
|
@ -126,7 +129,9 @@ class ActionListPresenter @Inject constructor(
|
||||||
else -> buildList<TimelineItemAction> {
|
else -> buildList<TimelineItemAction> {
|
||||||
if (timelineItem.isRemote) {
|
if (timelineItem.isRemote) {
|
||||||
// Can only reply or forward messages already uploaded to the server
|
// Can only reply or forward messages already uploaded to the server
|
||||||
add(TimelineItemAction.Reply)
|
if (userCanSendMessage) {
|
||||||
|
add(TimelineItemAction.Reply)
|
||||||
|
}
|
||||||
add(TimelineItemAction.Forward)
|
add(TimelineItemAction.Forward)
|
||||||
}
|
}
|
||||||
if (timelineItem.isMine && timelineItem.isTextMessage) {
|
if (timelineItem.isMine && timelineItem.isTextMessage) {
|
||||||
|
|
|
||||||
|
|
@ -35,5 +35,5 @@ sealed class TimelineItemAction(
|
||||||
data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit)
|
data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit)
|
||||||
data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode)
|
data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode)
|
||||||
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true)
|
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true)
|
||||||
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.EndPoll)
|
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.PollEnd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ internal fun AttachmentsBottomSheet(
|
||||||
state: MessageComposerState,
|
state: MessageComposerState,
|
||||||
onSendLocationClicked: () -> Unit,
|
onSendLocationClicked: () -> Unit,
|
||||||
onCreatePollClicked: () -> Unit,
|
onCreatePollClicked: () -> Unit,
|
||||||
|
enableTextFormatting: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val localView = LocalView.current
|
val localView = LocalView.current
|
||||||
|
|
@ -87,6 +88,7 @@ internal fun AttachmentsBottomSheet(
|
||||||
) {
|
) {
|
||||||
AttachmentSourcePickerMenu(
|
AttachmentSourcePickerMenu(
|
||||||
state = state,
|
state = state,
|
||||||
|
enableTextFormatting = enableTextFormatting,
|
||||||
onSendLocationClicked = onSendLocationClicked,
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
onCreatePollClicked = onCreatePollClicked,
|
onCreatePollClicked = onCreatePollClicked,
|
||||||
)
|
)
|
||||||
|
|
@ -100,6 +102,7 @@ internal fun AttachmentSourcePickerMenu(
|
||||||
state: MessageComposerState,
|
state: MessageComposerState,
|
||||||
onSendLocationClicked: () -> Unit,
|
onSendLocationClicked: () -> Unit,
|
||||||
onCreatePollClicked: () -> Unit,
|
onCreatePollClicked: () -> Unit,
|
||||||
|
enableTextFormatting: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -146,11 +149,13 @@ internal fun AttachmentSourcePickerMenu(
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) },
|
text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ListItem(
|
if (enableTextFormatting) {
|
||||||
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) },
|
ListItem(
|
||||||
icon = { Icon(Icons.Default.FormatColorText, null) },
|
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) },
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) },
|
icon = { Icon(Icons.Default.FormatColorText, null) },
|
||||||
)
|
text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,5 +168,6 @@ internal fun AttachmentSourcePickerMenuPreview() = ElementPreview {
|
||||||
),
|
),
|
||||||
onSendLocationClicked = {},
|
onSendLocationClicked = {},
|
||||||
onCreatePollClicked = {},
|
onCreatePollClicked = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,14 +148,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
)
|
)
|
||||||
is MessageComposerEvents.SetMode -> {
|
is MessageComposerEvents.SetMode -> {
|
||||||
messageComposerContext.composerMode = event.composerMode
|
messageComposerContext.composerMode = event.composerMode
|
||||||
analyticsService.capture(
|
|
||||||
Composer(
|
|
||||||
inThread = messageComposerContext.composerMode.inThread,
|
|
||||||
isEditing = messageComposerContext.composerMode.isEditing,
|
|
||||||
isReply = messageComposerContext.composerMode.isReply,
|
|
||||||
isLocation = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
MessageComposerEvents.AddAttachment -> localCoroutineScope.launch {
|
MessageComposerEvents.AddAttachment -> localCoroutineScope.launch {
|
||||||
showAttachmentSourcePicker = true
|
showAttachmentSourcePicker = true
|
||||||
|
|
@ -238,6 +230,14 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
message.html,
|
message.html,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
analyticsService.capture(
|
||||||
|
Composer(
|
||||||
|
inThread = capturedMode.inThread,
|
||||||
|
isEditing = capturedMode.isEditing,
|
||||||
|
isReply = capturedMode.isReply,
|
||||||
|
messageType = Composer.MessageType.Text, // Set proper type when we'll be sending other types of messages.
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.sendAttachment(
|
private fun CoroutineScope.sendAttachment(
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ fun MessageComposerView(
|
||||||
state: MessageComposerState,
|
state: MessageComposerState,
|
||||||
onSendLocationClicked: () -> Unit,
|
onSendLocationClicked: () -> Unit,
|
||||||
onCreatePollClicked: () -> Unit,
|
onCreatePollClicked: () -> Unit,
|
||||||
|
enableTextFormatting: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun onFullscreenToggle() {
|
fun onFullscreenToggle() {
|
||||||
|
|
@ -62,6 +63,7 @@ fun MessageComposerView(
|
||||||
state = state,
|
state = state,
|
||||||
onSendLocationClicked = onSendLocationClicked,
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
onCreatePollClicked = onCreatePollClicked,
|
onCreatePollClicked = onCreatePollClicked,
|
||||||
|
enableTextFormatting = enableTextFormatting,
|
||||||
)
|
)
|
||||||
|
|
||||||
TextComposer(
|
TextComposer(
|
||||||
|
|
@ -74,6 +76,7 @@ fun MessageComposerView(
|
||||||
onResetComposerMode = ::onCloseSpecialMode,
|
onResetComposerMode = ::onCloseSpecialMode,
|
||||||
onAddAttachment = ::onAddAttachment,
|
onAddAttachment = ::onAddAttachment,
|
||||||
onDismissTextFormatting = ::onDismissTextFormatting,
|
onDismissTextFormatting = ::onDismissTextFormatting,
|
||||||
|
enableTextFormatting = enableTextFormatting,
|
||||||
onError = ::onError,
|
onError = ::onError,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -95,5 +98,6 @@ private fun ContentToPreview(state: MessageComposerState) {
|
||||||
state = state,
|
state = state,
|
||||||
onSendLocationClicked = {},
|
onSendLocationClicked = {},
|
||||||
onCreatePollClicked = {},
|
onCreatePollClicked = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import im.vector.app.features.analytics.plan.PollVote
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -51,6 +53,7 @@ class TimelinePresenter @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val appScope: CoroutineScope,
|
private val appScope: CoroutineScope,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
|
|
||||||
private val timeline = room.timeline
|
private val timeline = room.timeline
|
||||||
|
|
@ -93,7 +96,7 @@ class TimelinePresenter @Inject constructor(
|
||||||
pollStartId = event.pollStartId,
|
pollStartId = event.pollStartId,
|
||||||
answers = listOf(event.answerId),
|
answers = listOf(event.answerId),
|
||||||
)
|
)
|
||||||
// TODO Polls: Send poll vote analytic
|
analyticsService.capture(PollVote())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +119,7 @@ class TimelinePresenter @Inject constructor(
|
||||||
|
|
||||||
return TimelineState(
|
return TimelineState(
|
||||||
highlightedEventId = highlightedEventId.value,
|
highlightedEventId = highlightedEventId.value,
|
||||||
canReply = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
paginationState = paginationState,
|
paginationState = paginationState,
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
hasNewItems = hasNewItems.value,
|
hasNewItems = hasNewItems.value,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
data class TimelineState(
|
data class TimelineState(
|
||||||
val timelineItems: ImmutableList<TimelineItem>,
|
val timelineItems: ImmutableList<TimelineItem>,
|
||||||
val highlightedEventId: EventId?,
|
val highlightedEventId: EventId?,
|
||||||
val canReply: Boolean,
|
val userHasPermissionToSendMessage: Boolean,
|
||||||
val paginationState: MatrixTimeline.PaginationState,
|
val paginationState: MatrixTimeline.PaginationState,
|
||||||
val hasNewItems: Boolean,
|
val hasNewItems: Boolean,
|
||||||
val eventSink: (TimelineEvents) -> Unit
|
val eventSink: (TimelineEvents) -> Unit
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, hasMoreToLoadBackwards = true),
|
paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, hasMoreToLoadBackwards = true),
|
||||||
highlightedEventId = null,
|
highlightedEventId = null,
|
||||||
canReply = true,
|
userHasPermissionToSendMessage = true,
|
||||||
hasNewItems = false,
|
hasNewItems = false,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
||||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||||
|
|
@ -119,7 +120,7 @@ fun TimelineView(
|
||||||
TimelineItemRow(
|
TimelineItemRow(
|
||||||
timelineItem = timelineItem,
|
timelineItem = timelineItem,
|
||||||
highlightedItem = state.highlightedEventId?.value,
|
highlightedItem = state.highlightedEventId?.value,
|
||||||
canReply = state.canReply,
|
userHasPermissionToSendMessage = state.userHasPermissionToSendMessage,
|
||||||
onClick = onMessageClicked,
|
onClick = onMessageClicked,
|
||||||
onLongClick = onMessageLongClicked,
|
onLongClick = onMessageLongClicked,
|
||||||
onUserDataClick = onUserDataClicked,
|
onUserDataClick = onUserDataClicked,
|
||||||
|
|
@ -156,7 +157,7 @@ fun TimelineView(
|
||||||
fun TimelineItemRow(
|
fun TimelineItemRow(
|
||||||
timelineItem: TimelineItem,
|
timelineItem: TimelineItem,
|
||||||
highlightedItem: String?,
|
highlightedItem: String?,
|
||||||
canReply: Boolean,
|
userHasPermissionToSendMessage: Boolean,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -189,7 +190,7 @@ fun TimelineItemRow(
|
||||||
TimelineItemEventRow(
|
TimelineItemEventRow(
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||||
canReply = canReply,
|
canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(),
|
||||||
onClick = { onClick(timelineItem) },
|
onClick = { onClick(timelineItem) },
|
||||||
onLongClick = { onLongClick(timelineItem) },
|
onLongClick = { onLongClick(timelineItem) },
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
|
|
@ -228,7 +229,7 @@ fun TimelineItemRow(
|
||||||
TimelineItemRow(
|
TimelineItemRow(
|
||||||
timelineItem = subGroupEvent,
|
timelineItem = subGroupEvent,
|
||||||
highlightedItem = highlightedItem,
|
highlightedItem = highlightedItem,
|
||||||
canReply = false,
|
userHasPermissionToSendMessage = false,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,14 @@ package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.times
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
import io.element.android.libraries.designsystem.text.toDp
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -69,3 +73,10 @@ fun ExtraPadding.getStr(fontSize: TextUnit): String {
|
||||||
// A space and some unbreakable spaces
|
// A space and some unbreakable spaces
|
||||||
return " " + "\u00A0".repeat(nbOfSpaces)
|
return " " + "\u00A0".repeat(nbOfSpaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExtraPadding.getDpSize(): Dp {
|
||||||
|
if (nbChars == 0) return 0.dp
|
||||||
|
val timestampFontSize = ElementTheme.typography.fontBodyXsRegular.fontSize // 11.sp
|
||||||
|
return nbChars * timestampFontSize.toDp() / 3
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,6 @@ package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
|
@ -28,7 +25,6 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.messages.impl.timeline.components.html.HtmlDocument
|
import io.element.android.features.messages.impl.timeline.components.html.HtmlDocument
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
|
@ -51,18 +47,14 @@ fun TimelineItemTextView(
|
||||||
CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textPrimary) {
|
CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textPrimary) {
|
||||||
val htmlDocument = content.htmlDocument
|
val htmlDocument = content.htmlDocument
|
||||||
if (htmlDocument != null) {
|
if (htmlDocument != null) {
|
||||||
// For now we ignore the extra padding for html content, so add some spacing
|
HtmlDocument(
|
||||||
// below the content (as previous behavior)
|
document = htmlDocument,
|
||||||
Column(modifier = modifier) {
|
extraPadding = extraPadding,
|
||||||
HtmlDocument(
|
modifier = modifier,
|
||||||
document = htmlDocument,
|
onTextClicked = onTextClicked,
|
||||||
modifier = Modifier,
|
onTextLongClicked = onTextLongClicked,
|
||||||
onTextClicked = onTextClicked,
|
interactionSource = interactionSource
|
||||||
onTextLongClicked = onTextLongClicked,
|
)
|
||||||
interactionSource = interactionSource
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
val textWithPadding = remember(content.body) {
|
val textWithPadding = remember(content.body) {
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,10 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
import androidx.compose.foundation.text.appendInlineContent
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
|
|
@ -53,13 +55,18 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.event.ExtraPadding
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.event.getDpSize
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding
|
||||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
import io.element.android.libraries.designsystem.text.toDp
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.theme.LinkColor
|
import io.element.android.libraries.theme.LinkColor
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
|
|
@ -72,18 +79,28 @@ private const val CHIP_ID = "chip"
|
||||||
@Composable
|
@Composable
|
||||||
fun HtmlDocument(
|
fun HtmlDocument(
|
||||||
document: Document,
|
document: Document,
|
||||||
|
extraPadding: ExtraPadding,
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onTextClicked: () -> Unit = {},
|
onTextClicked: () -> Unit = {},
|
||||||
onTextLongClicked: () -> Unit = {},
|
onTextLongClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
HtmlBody(
|
FlowRow(
|
||||||
body = document.body(),
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onTextClicked = onTextClicked,
|
) {
|
||||||
onTextLongClicked = onTextLongClicked,
|
HtmlBody(
|
||||||
)
|
body = document.body(),
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onTextClicked = onTextClicked,
|
||||||
|
onTextLongClicked = onTextLongClicked,
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier.size(
|
||||||
|
width = extraPadding.getDpSize(),
|
||||||
|
height = ElementTheme.typography.fontBodyXsRegular.fontSize.toDp() * 1.25f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -603,5 +620,9 @@ internal fun HtmlDocumentDarkPreview(@PreviewParameter(DocumentProvider::class)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(document: Document) {
|
private fun ContentToPreview(document: Document) {
|
||||||
HtmlDocument(document, remember { MutableInteractionSource() })
|
HtmlDocument(
|
||||||
|
document = document,
|
||||||
|
extraPadding = noExtraPadding,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,18 @@ fun TimelineItemEventContent.canBeCopied(): Boolean =
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the event content can be replied to.
|
||||||
|
* Note: it should match the logic in [io.element.android.features.messages.impl.actionlist.ActionListPresenter].
|
||||||
|
*/
|
||||||
|
fun TimelineItemEventContent.canBeRepliedTo(): Boolean =
|
||||||
|
when (this) {
|
||||||
|
is TimelineItemRedactedContent,
|
||||||
|
is TimelineItemStateContent,
|
||||||
|
is TimelineItemPollContent -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if user can react (i.e. send a reaction) on the event content.
|
* Return true if user can react (i.e. send a reaction) on the event content.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import im.vector.app.features.analytics.plan.PollEnd
|
||||||
import io.element.android.features.messages.fixtures.aMessageEvent
|
import io.element.android.features.messages.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.InviteDialogAction
|
import io.element.android.features.messages.impl.InviteDialogAction
|
||||||
|
|
@ -575,7 +576,11 @@ class MessagesPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle poll end`() = runTest {
|
fun `present - handle poll end`() = runTest {
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = createMessagePresenter(matrixRoom = room)
|
val analyticsService = FakeAnalyticsService()
|
||||||
|
val presenter = createMessagePresenter(
|
||||||
|
matrixRoom = room,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -586,7 +591,8 @@ class MessagesPresenterTest {
|
||||||
assertThat(room.endPollInvocations.size).isEqualTo(1)
|
assertThat(room.endPollInvocations.size).isEqualTo(1)
|
||||||
assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
|
assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
|
||||||
assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.")
|
assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.")
|
||||||
// TODO Polls: Test poll end analytic
|
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
||||||
|
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -595,6 +601,7 @@ class MessagesPresenterTest {
|
||||||
matrixRoom: MatrixRoom = FakeMatrixRoom(),
|
matrixRoom: MatrixRoom = FakeMatrixRoom(),
|
||||||
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||||
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||||
|
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||||
): MessagesPresenter {
|
): MessagesPresenter {
|
||||||
val messageComposerPresenter = MessageComposerPresenter(
|
val messageComposerPresenter = MessageComposerPresenter(
|
||||||
appCoroutineScope = this,
|
appCoroutineScope = this,
|
||||||
|
|
@ -604,7 +611,7 @@ class MessagesPresenterTest {
|
||||||
localMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
|
localMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
|
||||||
mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom),
|
mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
analyticsService = FakeAnalyticsService(),
|
analyticsService = analyticsService,
|
||||||
messageComposerContext = MessageComposerContextImpl(),
|
messageComposerContext = MessageComposerContextImpl(),
|
||||||
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
||||||
|
|
||||||
|
|
@ -613,13 +620,15 @@ class MessagesPresenterTest {
|
||||||
timelineItemsFactory = aTimelineItemsFactory(),
|
timelineItemsFactory = aTimelineItemsFactory(),
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
appScope = this
|
appScope = this,
|
||||||
|
analyticsService = analyticsService,
|
||||||
)
|
)
|
||||||
val buildMeta = aBuildMeta()
|
val buildMeta = aBuildMeta()
|
||||||
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
|
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
|
||||||
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||||
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
|
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
|
||||||
|
val featureFlagsService = FakeFeatureFlagService(mapOf(FeatureFlags.RichTextEditor.key to true))
|
||||||
return MessagesPresenter(
|
return MessagesPresenter(
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
composerPresenter = messageComposerPresenter,
|
composerPresenter = messageComposerPresenter,
|
||||||
|
|
@ -633,6 +642,8 @@ class MessagesPresenterTest {
|
||||||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
clipboardHelper = clipboardHelper,
|
clipboardHelper = clipboardHelper,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
featureFlagService = featureFlagsService,
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class ActionListPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
|
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -89,7 +89,7 @@ class ActionListPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
|
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -117,7 +117,7 @@ class ActionListPresenterTest {
|
||||||
isMine = false,
|
isMine = false,
|
||||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -138,6 +138,37 @@ class ActionListPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute for others message cannot sent message`() = runTest {
|
||||||
|
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
isMine = false,
|
||||||
|
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = false))
|
||||||
|
// val loadingState = awaitItem()
|
||||||
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.target).isEqualTo(
|
||||||
|
ActionListState.Target.Success(
|
||||||
|
messageEvent,
|
||||||
|
persistentListOf(
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Copy,
|
||||||
|
TimelineItemAction.Developer,
|
||||||
|
TimelineItemAction.ReportContent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute for others message and can redact`() = runTest {
|
fun `present - compute for others message and can redact`() = runTest {
|
||||||
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||||
|
|
@ -149,7 +180,7 @@ class ActionListPresenterTest {
|
||||||
isMine = false,
|
isMine = false,
|
||||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, true))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
@ -180,7 +211,7 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -213,7 +244,7 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemImageContent(),
|
content = aTimelineItemImageContent(),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -244,7 +275,7 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemStateEventContent(),
|
content = aTimelineItemStateEventContent(),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true))
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -273,7 +304,7 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemStateEventContent(),
|
content = aTimelineItemStateEventContent(),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true))
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -301,7 +332,7 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||||
// val loadingState = awaitItem()
|
// val loadingState = awaitItem()
|
||||||
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
|
|
@ -338,10 +369,10 @@ class ActionListPresenterTest {
|
||||||
content = TimelineItemRedactedContent,
|
content = TimelineItemRedactedContent,
|
||||||
)
|
)
|
||||||
|
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||||
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
|
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
|
||||||
|
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, canRedact = false, canSendMessage = true))
|
||||||
awaitItem().run {
|
awaitItem().run {
|
||||||
assertThat(target).isEqualTo(ActionListState.Target.None)
|
assertThat(target).isEqualTo(ActionListState.Target.None)
|
||||||
assertThat(displayEmojiReactions).isFalse()
|
assertThat(displayEmojiReactions).isFalse()
|
||||||
|
|
@ -362,7 +393,7 @@ class ActionListPresenterTest {
|
||||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
|
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
|
||||||
)
|
)
|
||||||
|
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
@ -389,7 +420,7 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemPollContent(),
|
content = aTimelineItemPollContent(),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
@ -415,7 +446,7 @@ class ActionListPresenterTest {
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemPollContent(isEnded = true),
|
content = aTimelineItemPollContent(isEnded = true),
|
||||||
)
|
)
|
||||||
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.target).isEqualTo(
|
assertThat(successState.target).isEqualTo(
|
||||||
ActionListState.Target.Success(
|
ActionListState.Target.Success(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import im.vector.app.features.analytics.plan.PollVote
|
||||||
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
|
|
@ -37,6 +38,7 @@ import io.element.android.libraries.matrix.test.room.aMessageContent
|
||||||
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitWithLatch
|
import io.element.android.tests.testutils.awaitWithLatch
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
|
@ -260,7 +262,11 @@ class TimelinePresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - PollAnswerSelected event calls into rust room api and analytics`() = runTest {
|
fun `present - PollAnswerSelected event calls into rust room api and analytics`() = runTest {
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = createTimelinePresenter(room)
|
val analyticsService = FakeAnalyticsService()
|
||||||
|
val presenter = createTimelinePresenter(
|
||||||
|
room = room,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -271,7 +277,8 @@ class TimelinePresenterTest {
|
||||||
assertThat(room.sendPollResponseInvocations.size).isEqualTo(1)
|
assertThat(room.sendPollResponseInvocations.size).isEqualTo(1)
|
||||||
assertThat(room.sendPollResponseInvocations.first().answers).isEqualTo(listOf("anAnswerId"))
|
assertThat(room.sendPollResponseInvocations.first().answers).isEqualTo(listOf("anAnswerId"))
|
||||||
assertThat(room.sendPollResponseInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
|
assertThat(room.sendPollResponseInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
|
||||||
// TODO Polls: Test poll vote analytic
|
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
|
||||||
|
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollVote())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createTimelinePresenter(
|
private fun TestScope.createTimelinePresenter(
|
||||||
|
|
@ -282,18 +289,21 @@ class TimelinePresenterTest {
|
||||||
timelineItemsFactory = timelineItemsFactory,
|
timelineItemsFactory = timelineItemsFactory,
|
||||||
room = FakeMatrixRoom(matrixTimeline = timeline),
|
room = FakeMatrixRoom(matrixTimeline = timeline),
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
appScope = this
|
appScope = this,
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createTimelinePresenter(
|
private fun TestScope.createTimelinePresenter(
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
|
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
|
||||||
): TimelinePresenter {
|
): TimelinePresenter {
|
||||||
return TimelinePresenter(
|
return TimelinePresenter(
|
||||||
timelineItemsFactory = aTimelineItemsFactory(),
|
timelineItemsFactory = aTimelineItemsFactory(),
|
||||||
room = room,
|
room = room,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
appScope = this
|
appScope = this,
|
||||||
|
analyticsService = analyticsService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Poll
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -86,13 +84,13 @@ internal fun PollTitle(
|
||||||
) {
|
) {
|
||||||
if (isPollEnded) {
|
if (isPollEnded) {
|
||||||
Icon(
|
Icon(
|
||||||
resourceId = VectorIcons.EndPoll,
|
resourceId = VectorIcons.PollEnd,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Poll,
|
resourceId = VectorIcons.Poll,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ dependencies {
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
|
implementation(projects.features.messages.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
|
|
@ -48,6 +49,7 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
testImplementation(projects.features.messages.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,17 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
class CreatePollNode @AssistedInject constructor(
|
class CreatePollNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
presenterFactory: CreatePollPresenter.Factory,
|
presenterFactory: CreatePollPresenter.Factory,
|
||||||
// analyticsService: AnalyticsService, // TODO Polls: add analytics
|
analyticsService: AnalyticsService,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
private val presenter = presenterFactory.create(backNavigator = ::navigateUp)
|
private val presenter = presenterFactory.create(backNavigator = ::navigateUp)
|
||||||
|
|
@ -40,8 +42,7 @@ class CreatePollNode @AssistedInject constructor(
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onResume = {
|
onResume = {
|
||||||
// TODO Polls: add analytics
|
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.CreatePollView))
|
||||||
// analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.PollView))
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,13 @@ import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
|
import im.vector.app.features.analytics.plan.PollCreation
|
||||||
|
import io.element.android.features.messages.api.MessageComposerContext
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -44,9 +48,9 @@ private const val MAX_SELECTIONS = 1
|
||||||
|
|
||||||
class CreatePollPresenter @AssistedInject constructor(
|
class CreatePollPresenter @AssistedInject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
// private val analyticsService: AnalyticsService, // TODO Polls: add analytics
|
private val analyticsService: AnalyticsService,
|
||||||
|
private val messageComposerContext: MessageComposerContext,
|
||||||
@Assisted private val navigateUp: () -> Unit,
|
@Assisted private val navigateUp: () -> Unit,
|
||||||
// private val messageComposerContext: MessageComposerContext, // TODO Polls: add analytics
|
|
||||||
) : Presenter<CreatePollState> {
|
) : Presenter<CreatePollState> {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -78,7 +82,21 @@ class CreatePollPresenter @AssistedInject constructor(
|
||||||
maxSelections = MAX_SELECTIONS,
|
maxSelections = MAX_SELECTIONS,
|
||||||
pollKind = pollKind,
|
pollKind = pollKind,
|
||||||
)
|
)
|
||||||
// analyticsService.capture(PollCreate()) // TODO Polls: add analytics
|
analyticsService.capture(
|
||||||
|
Composer(
|
||||||
|
inThread = messageComposerContext.composerMode.inThread,
|
||||||
|
isEditing = messageComposerContext.composerMode.isEditing,
|
||||||
|
isReply = messageComposerContext.composerMode.isReply,
|
||||||
|
messageType = Composer.MessageType.Poll,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
analyticsService.capture(
|
||||||
|
PollCreation(
|
||||||
|
action = PollCreation.Action.Create,
|
||||||
|
isUndisclosed = pollKind == PollKind.Undisclosed,
|
||||||
|
numberOfAnswers = answers.size,
|
||||||
|
)
|
||||||
|
)
|
||||||
navigateUp()
|
navigateUp()
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Cannot create poll")
|
Timber.d("Cannot create poll")
|
||||||
|
|
@ -153,7 +171,7 @@ private val pollKindSaver: Saver<MutableState<PollKind>, Boolean> = Saver(
|
||||||
},
|
},
|
||||||
restore = {
|
restore = {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
when(it) {
|
when (it) {
|
||||||
true -> PollKind.Undisclosed
|
true -> PollKind.Undisclosed
|
||||||
else -> PollKind.Disclosed
|
else -> PollKind.Disclosed
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,13 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
|
import im.vector.app.features.analytics.plan.PollCreation
|
||||||
|
import io.element.android.features.messages.test.MessageComposerContextFake
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.test.room.CreatePollInvocation
|
import io.element.android.libraries.matrix.test.room.CreatePollInvocation
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -37,11 +41,13 @@ class CreatePollPresenterTest {
|
||||||
|
|
||||||
private var navUpInvocationsCount = 0
|
private var navUpInvocationsCount = 0
|
||||||
private val fakeMatrixRoom = FakeMatrixRoom()
|
private val fakeMatrixRoom = FakeMatrixRoom()
|
||||||
// private val fakeAnalyticsService = FakeAnalyticsService() // TODO Polls: add analytics
|
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||||
|
private val messageComposerContextFake = MessageComposerContextFake()
|
||||||
|
|
||||||
private val presenter = CreatePollPresenter(
|
private val presenter = CreatePollPresenter(
|
||||||
room = fakeMatrixRoom,
|
room = fakeMatrixRoom,
|
||||||
// analyticsService = fakeAnalyticsService, // TODO Polls: add analytics
|
analyticsService = fakeAnalyticsService,
|
||||||
|
messageComposerContext = messageComposerContextFake,
|
||||||
navigateUp = { navUpInvocationsCount++ },
|
navigateUp = { navUpInvocationsCount++ },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -104,6 +110,22 @@ class CreatePollPresenterTest {
|
||||||
pollKind = PollKind.Disclosed
|
pollKind = PollKind.Disclosed
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
Truth.assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2)
|
||||||
|
Truth.assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo(
|
||||||
|
Composer(
|
||||||
|
inThread = false,
|
||||||
|
isEditing = false,
|
||||||
|
isReply = false,
|
||||||
|
messageType = Composer.MessageType.Poll,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Truth.assertThat(fakeAnalyticsService.capturedEvents[1]).isEqualTo(
|
||||||
|
PollCreation(
|
||||||
|
action = PollCreation.Action.Create,
|
||||||
|
isUndisclosed = false,
|
||||||
|
numberOfAnswers = 2,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,13 +158,19 @@ private fun DefaultRoomListTopBar(
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
.avatarBloom(
|
.avatarBloom(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
background = ElementTheme.materialColors.background,
|
background = if (ElementTheme.isLightTheme) {
|
||||||
|
// Workaround to display a very subtle bloom for avatars with very soft colors
|
||||||
|
Color(0xFFF9F9F9)
|
||||||
|
} else {
|
||||||
|
ElementTheme.materialColors.background
|
||||||
|
},
|
||||||
blurSize = DpSize(avatarBloomSize, avatarBloomSize),
|
blurSize = DpSize(avatarBloomSize, avatarBloomSize),
|
||||||
offset = DpOffset(24.dp, 24.dp + statusBarPadding),
|
offset = DpOffset(24.dp, 24.dp + statusBarPadding),
|
||||||
clipToSize = if (appBarHeight > 0) DpSize(
|
clipToSize = if (appBarHeight > 0) DpSize(
|
||||||
avatarBloomSize,
|
avatarBloomSize,
|
||||||
appBarHeight.toDp()
|
appBarHeight.toDp()
|
||||||
) else DpSize.Unspecified,
|
) else DpSize.Unspecified,
|
||||||
|
bottomSoftEdgeColor = ElementTheme.materialColors.background,
|
||||||
bottomSoftEdgeAlpha = 1f - collapsedFraction,
|
bottomSoftEdgeAlpha = 1f - collapsedFraction,
|
||||||
alpha = if (areSearchResultsDisplayed) 0f else 1f,
|
alpha = if (areSearchResultsDisplayed) 0f else 1f,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ dependencyanalysis = "1.21.0"
|
||||||
stem = "2.3.0"
|
stem = "2.3.0"
|
||||||
sqldelight = "1.5.5"
|
sqldelight = "1.5.5"
|
||||||
telephoto = "0.6.0"
|
telephoto = "0.6.0"
|
||||||
wysiwyg = "2.9.0"
|
wysiwyg = "2.10.0"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
dagger = "2.48"
|
dagger = "2.48"
|
||||||
|
|
@ -149,7 +149,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||||
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
||||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.50"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.51"
|
||||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", 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-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||||
|
|
@ -169,7 +169,7 @@ maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1"
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog.android:posthog:2.0.3"
|
posthog = "com.posthog.android:posthog:2.0.3"
|
||||||
sentry = "io.sentry:sentry-android:6.29.0"
|
sentry = "io.sentry:sentry-android:6.29.0"
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:e9cd9adaf18cec52ed851395eb84358b4f9b8d7f"
|
||||||
|
|
||||||
# Emojibase
|
# Emojibase
|
||||||
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"
|
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ object VectorIcons {
|
||||||
val ReportContent = R.drawable.ic_report_content
|
val ReportContent = R.drawable.ic_report_content
|
||||||
val Groups = R.drawable.ic_groups
|
val Groups = R.drawable.ic_groups
|
||||||
val Share = R.drawable.ic_share
|
val Share = R.drawable.ic_share
|
||||||
val EndPoll = R.drawable.ic_poll_end
|
val Poll = R.drawable.ic_poll
|
||||||
|
val PollEnd = R.drawable.ic_poll_end
|
||||||
val Bold = R.drawable.ic_bold
|
val Bold = R.drawable.ic_bold
|
||||||
val BulletList = R.drawable.ic_bullet_list
|
val BulletList = R.drawable.ic_bullet_list
|
||||||
val CodeBlock = R.drawable.ic_code_block
|
val CodeBlock = R.drawable.ic_code_block
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -96,8 +97,10 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.isSpecified
|
import androidx.compose.ui.unit.isSpecified
|
||||||
import androidx.compose.ui.unit.toOffset
|
import androidx.compose.ui.unit.toOffset
|
||||||
import androidx.compose.ui.unit.toSize
|
import androidx.compose.ui.unit.toSize
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.imageLoader
|
||||||
|
import coil.request.DefaultRequestOptions
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import coil.size.Size
|
||||||
import com.airbnb.android.showkase.annotation.ShowkaseComposable
|
import com.airbnb.android.showkase.annotation.ShowkaseComposable
|
||||||
import com.vanniktech.blurhash.BlurHash
|
import com.vanniktech.blurhash.BlurHash
|
||||||
import io.element.android.libraries.designsystem.R
|
import io.element.android.libraries.designsystem.R
|
||||||
|
|
@ -114,6 +117,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|
@ -125,7 +130,7 @@ object BloomDefaults {
|
||||||
* Number of components to use with BlurHash to generate the blur effect.
|
* Number of components to use with BlurHash to generate the blur effect.
|
||||||
* Larger values mean more detailed blurs.
|
* Larger values mean more detailed blurs.
|
||||||
*/
|
*/
|
||||||
const val HASH_COMPONENTS = 8
|
const val HASH_COMPONENTS = 5
|
||||||
|
|
||||||
/** Default bloom layers. */
|
/** Default bloom layers. */
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -159,6 +164,7 @@ data class BloomLayer(
|
||||||
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
|
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
|
||||||
* @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
|
* @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
|
||||||
* @param layerConfiguration The configuration for the bloom layers. If not specified the default layers configuration will be used.
|
* @param layerConfiguration The configuration for the bloom layers. If not specified the default layers configuration will be used.
|
||||||
|
* @param bottomSoftEdgeColor The color to use for the bottom soft edge. If not specified the [background] color will be used.
|
||||||
* @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
|
* @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
|
||||||
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
|
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
|
||||||
* @param alpha The alpha value to apply to the bloom effect.
|
* @param alpha The alpha value to apply to the bloom effect.
|
||||||
|
|
@ -170,6 +176,7 @@ fun Modifier.bloom(
|
||||||
offset: DpOffset = DpOffset.Unspecified,
|
offset: DpOffset = DpOffset.Unspecified,
|
||||||
clipToSize: DpSize = DpSize.Unspecified,
|
clipToSize: DpSize = DpSize.Unspecified,
|
||||||
layerConfiguration: ImmutableList<BloomLayer>? = null,
|
layerConfiguration: ImmutableList<BloomLayer>? = null,
|
||||||
|
bottomSoftEdgeColor: Color = background,
|
||||||
bottomSoftEdgeHeight: Dp = 40.dp,
|
bottomSoftEdgeHeight: Dp = 40.dp,
|
||||||
@FloatRange(from = 0.0, to = 1.0)
|
@FloatRange(from = 0.0, to = 1.0)
|
||||||
bottomSoftEdgeAlpha: Float = 1.0f,
|
bottomSoftEdgeAlpha: Float = 1.0f,
|
||||||
|
|
@ -238,7 +245,7 @@ fun Modifier.bloom(
|
||||||
val bottomEdgeGradient = LinearGradientShader(
|
val bottomEdgeGradient = LinearGradientShader(
|
||||||
from = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeightPixels).toOffset(),
|
from = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeightPixels).toOffset(),
|
||||||
to = IntOffset(0, clipToPixelSize.height).toOffset(),
|
to = IntOffset(0, clipToPixelSize.height).toOffset(),
|
||||||
listOf(Color.Transparent, background),
|
listOf(Color.Transparent, bottomSoftEdgeColor),
|
||||||
listOf(0f, 1f)
|
listOf(0f, 1f)
|
||||||
)
|
)
|
||||||
val bottomEdgeGradientBrush = ShaderBrush(bottomEdgeGradient)
|
val bottomEdgeGradientBrush = ShaderBrush(bottomEdgeGradient)
|
||||||
|
|
@ -297,6 +304,7 @@ fun Modifier.bloom(
|
||||||
* @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component.
|
* @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component.
|
||||||
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
|
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
|
||||||
* @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
|
* @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
|
||||||
|
* @param bottomSoftEdgeColor The color to use for the bottom soft edge. If not specified the [background] color will be used.
|
||||||
* @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
|
* @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
|
||||||
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
|
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
|
||||||
* @param alpha The alpha value to apply to the bloom effect.
|
* @param alpha The alpha value to apply to the bloom effect.
|
||||||
|
|
@ -307,6 +315,7 @@ fun Modifier.avatarBloom(
|
||||||
blurSize: DpSize = DpSize.Unspecified,
|
blurSize: DpSize = DpSize.Unspecified,
|
||||||
offset: DpOffset = DpOffset.Unspecified,
|
offset: DpOffset = DpOffset.Unspecified,
|
||||||
clipToSize: DpSize = DpSize.Unspecified,
|
clipToSize: DpSize = DpSize.Unspecified,
|
||||||
|
bottomSoftEdgeColor: Color = background,
|
||||||
bottomSoftEdgeHeight: Dp = 40.dp,
|
bottomSoftEdgeHeight: Dp = 40.dp,
|
||||||
@FloatRange(from = 0.0, to = 1.0)
|
@FloatRange(from = 0.0, to = 1.0)
|
||||||
bottomSoftEdgeAlpha: Float = 1.0f,
|
bottomSoftEdgeAlpha: Float = 1.0f,
|
||||||
|
|
@ -317,21 +326,35 @@ fun Modifier.avatarBloom(
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this
|
||||||
avatarData ?: return@composed this
|
avatarData ?: return@composed this
|
||||||
|
|
||||||
|
// Request the avatar contents to use as the bloom source
|
||||||
|
val context = LocalContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
if (avatarData.url != null) {
|
if (avatarData.url != null) {
|
||||||
// Request the avatar contents to use as the bloom source
|
val painterRequest = remember(avatarData) {
|
||||||
val painter = rememberAsyncImagePainter(
|
ImageRequest.Builder(context)
|
||||||
ImageRequest.Builder(LocalContext.current)
|
|
||||||
.data(avatarData)
|
.data(avatarData)
|
||||||
|
// Allow cache and default dispatchers
|
||||||
|
.defaults(DefaultRequestOptions())
|
||||||
// Needed to be able to read pixels from the Bitmap for the hash
|
// Needed to be able to read pixels from the Bitmap for the hash
|
||||||
.allowHardware(false)
|
.allowHardware(false)
|
||||||
|
// Reduce size so it loads faster for large avatars
|
||||||
|
.size(with(density) { Size(64.dp.roundToPx(), 64.dp.roundToPx()) })
|
||||||
.build()
|
.build()
|
||||||
)
|
}
|
||||||
var blurHash by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
|
// By making it saveable, we'll 'cache' the previous bloom effect until a new one is loaded
|
||||||
|
var blurHash by rememberSaveable(avatarData) { mutableStateOf<String?>(null) }
|
||||||
LaunchedEffect(avatarData) {
|
LaunchedEffect(avatarData) {
|
||||||
val drawable = painter.imageLoader.execute(painter.request).drawable ?: return@LaunchedEffect
|
withContext(Dispatchers.IO) {
|
||||||
val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@LaunchedEffect
|
val drawable =
|
||||||
blurHash = BlurHash.encode(bitmap, BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)
|
context.imageLoader.execute(painterRequest).drawable ?: return@withContext
|
||||||
|
val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@withContext
|
||||||
|
blurHash = BlurHash.encode(
|
||||||
|
bitmap,
|
||||||
|
BloomDefaults.HASH_COMPONENTS,
|
||||||
|
BloomDefaults.HASH_COMPONENTS
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bloom(
|
bloom(
|
||||||
|
|
@ -363,6 +386,7 @@ fun Modifier.avatarBloom(
|
||||||
blurSize = blurSize,
|
blurSize = blurSize,
|
||||||
offset = offset,
|
offset = offset,
|
||||||
clipToSize = clipToSize,
|
clipToSize = clipToSize,
|
||||||
|
bottomSoftEdgeColor = bottomSoftEdgeColor,
|
||||||
bottomSoftEdgeHeight = bottomSoftEdgeHeight,
|
bottomSoftEdgeHeight = bottomSoftEdgeHeight,
|
||||||
bottomSoftEdgeAlpha = bottomSoftEdgeAlpha,
|
bottomSoftEdgeAlpha = bottomSoftEdgeAlpha,
|
||||||
alpha = alpha,
|
alpha = alpha,
|
||||||
|
|
@ -517,7 +541,13 @@ internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorStateProvider::
|
||||||
modifier = Modifier.size(256.dp)
|
modifier = Modifier.size(256.dp)
|
||||||
.bloom(
|
.bloom(
|
||||||
hash = hash,
|
hash = hash,
|
||||||
background = ElementTheme.materialColors.background,
|
background = if (ElementTheme.isLightTheme) {
|
||||||
|
// Workaround to display a very subtle bloom for avatars with very soft colors
|
||||||
|
Color(0xFFF9F9F9)
|
||||||
|
} else {
|
||||||
|
ElementTheme.materialColors.background
|
||||||
|
},
|
||||||
|
bottomSoftEdgeColor = ElementTheme.materialColors.background,
|
||||||
blurSize = DpSize(256.dp, 256.dp),
|
blurSize = DpSize(256.dp, 256.dp),
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
|
|
||||||
9
libraries/designsystem/src/main/res/drawable/ic_poll.xml
Normal file
9
libraries/designsystem/src/main/res/drawable/ic_poll.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="22dp"
|
||||||
|
android:height="22dp"
|
||||||
|
android:viewportWidth="22"
|
||||||
|
android:viewportHeight="22">
|
||||||
|
<path
|
||||||
|
android:pathData="M7.333,15.583C7.593,15.583 7.811,15.495 7.986,15.32C8.162,15.144 8.25,14.926 8.25,14.667V10.083C8.25,9.824 8.162,9.606 7.986,9.43C7.811,9.255 7.593,9.167 7.333,9.167C7.074,9.167 6.856,9.255 6.68,9.43C6.505,9.606 6.417,9.824 6.417,10.083V14.667C6.417,14.926 6.505,15.144 6.68,15.32C6.856,15.495 7.074,15.583 7.333,15.583ZM11,15.583C11.26,15.583 11.477,15.495 11.653,15.32C11.829,15.144 11.917,14.926 11.917,14.667V7.333C11.917,7.074 11.829,6.856 11.653,6.68C11.477,6.505 11.26,6.417 11,6.417C10.74,6.417 10.523,6.505 10.347,6.68C10.171,6.856 10.083,7.074 10.083,7.333V14.667C10.083,14.926 10.171,15.144 10.347,15.32C10.523,15.495 10.74,15.583 11,15.583ZM14.667,15.583C14.926,15.583 15.144,15.495 15.32,15.32C15.495,15.144 15.583,14.926 15.583,14.667V12.833C15.583,12.574 15.495,12.356 15.32,12.18C15.144,12.005 14.926,11.917 14.667,11.917C14.407,11.917 14.189,12.005 14.014,12.18C13.838,12.356 13.75,12.574 13.75,12.833V14.667C13.75,14.926 13.838,15.144 14.014,15.32C14.189,15.495 14.407,15.583 14.667,15.583ZM4.583,19.25C4.079,19.25 3.648,19.07 3.289,18.712C2.93,18.352 2.75,17.921 2.75,17.417V4.583C2.75,4.079 2.93,3.648 3.289,3.289C3.648,2.93 4.079,2.75 4.583,2.75H17.417C17.921,2.75 18.352,2.93 18.712,3.289C19.07,3.648 19.25,4.079 19.25,4.583V17.417C19.25,17.921 19.07,18.352 18.712,18.712C18.352,19.07 17.921,19.25 17.417,19.25H4.583ZM4.583,17.417H17.417V4.583H4.583V17.417Z"
|
||||||
|
android:fillColor="#1B1D22"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -1,14 +1,21 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="20dp"
|
android:width="22dp"
|
||||||
android:height="19dp"
|
android:height="22dp"
|
||||||
android:viewportWidth="20"
|
android:viewportWidth="22"
|
||||||
android:viewportHeight="19">
|
android:viewportHeight="22">
|
||||||
<path
|
<path
|
||||||
android:pathData="M18,8.659V17C18,17.55 17.804,18.021 17.413,18.413C17.021,18.804 16.55,19 16,19H2C1.45,19 0.979,18.804 0.587,18.413C0.196,18.021 0,17.55 0,17V3C0,2.45 0.196,1.979 0.587,1.587C0.979,1.196 1.45,1 2,1H10.341C10.12,1.626 10,2.299 10,3L2,3V17H16V9C16.701,9 17.374,8.88 18,8.659ZM5.713,14.712C5.521,14.904 5.283,15 5,15C4.717,15 4.479,14.904 4.287,14.712C4.096,14.521 4,14.283 4,14V9C4,8.717 4.096,8.479 4.287,8.288C4.479,8.096 4.717,8 5,8C5.283,8 5.521,8.096 5.713,8.288C5.904,8.479 6,8.717 6,9V14C6,14.283 5.904,14.521 5.713,14.712ZM9.712,14.712C9.521,14.904 9.283,15 9,15C8.717,15 8.479,14.904 8.288,14.712C8.096,14.521 8,14.283 8,14V6C8,5.717 8.096,5.479 8.288,5.287C8.479,5.096 8.717,5 9,5C9.283,5 9.521,5.096 9.712,5.287C9.904,5.479 10,5.717 10,6V14C10,14.283 9.904,14.521 9.712,14.712ZM13.712,14.712C13.521,14.904 13.283,15 13,15C12.717,15 12.479,14.904 12.288,14.712C12.096,14.521 12,14.283 12,14V12C12,11.717 12.096,11.479 12.288,11.288C12.479,11.096 12.717,11 13,11C13.283,11 13.521,11.096 13.712,11.288C13.904,11.479 14,11.717 14,12V14C14,14.283 13.904,14.521 13.712,14.712Z"
|
android:pathData="M17.148,7.065L20.815,3.399C21.173,3.041 21.173,2.46 20.815,2.102C20.457,1.744 19.876,1.744 19.518,2.102L16.5,5.121L15.315,3.936C14.957,3.578 14.377,3.578 14.019,3.936C13.66,4.294 13.66,4.874 14.019,5.232L15.852,7.065C16.21,7.423 16.79,7.423 17.148,7.065Z"
|
||||||
android:fillColor="#1B1D22"
|
android:fillColor="#1B1D22"/>
|
||||||
android:fillType="evenOdd"/>
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M19.707,0.293C20.098,0.683 20.098,1.317 19.707,1.707L15.707,5.707C15.317,6.098 14.683,6.098 14.293,5.707L12.293,3.707C11.902,3.317 11.902,2.683 12.293,2.293C12.683,1.902 13.317,1.902 13.707,2.293L15,3.586L18.293,0.293C18.683,-0.098 19.317,-0.098 19.707,0.293Z"
|
android:pathData="M19.25,17.417V9.771C18.677,9.974 18.059,10.084 17.417,10.084V17.417H4.583V4.584L11.917,4.584C11.917,3.941 12.027,3.324 12.23,2.751H4.583C4.079,2.751 3.648,2.93 3.289,3.289C2.93,3.648 2.75,4.08 2.75,4.584V17.417C2.75,17.921 2.93,18.353 3.289,18.712C3.648,19.071 4.079,19.251 4.583,19.251H17.417C17.921,19.251 18.352,19.071 18.712,18.712C19.07,18.353 19.25,17.921 19.25,17.417Z"
|
||||||
android:fillColor="#1B1D22"
|
android:fillColor="#1B1D22"/>
|
||||||
android:fillType="evenOdd"/>
|
<path
|
||||||
|
android:pathData="M7.333,15.584C7.593,15.584 7.811,15.496 7.986,15.32C8.162,15.145 8.25,14.927 8.25,14.667V10.084C8.25,9.824 8.162,9.607 7.986,9.431C7.811,9.255 7.593,9.167 7.333,9.167C7.074,9.167 6.856,9.255 6.68,9.431C6.505,9.607 6.417,9.824 6.417,10.084V14.667C6.417,14.927 6.505,15.145 6.68,15.32C6.856,15.496 7.074,15.584 7.333,15.584Z"
|
||||||
|
android:fillColor="#1B1D22"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M11,15.584C11.26,15.584 11.477,15.496 11.653,15.32C11.829,15.145 11.917,14.927 11.917,14.667V7.334C11.917,7.074 11.829,6.857 11.653,6.681C11.477,6.505 11.26,6.417 11,6.417C10.74,6.417 10.523,6.505 10.347,6.681C10.171,6.857 10.083,7.074 10.083,7.334V14.667C10.083,14.927 10.171,15.145 10.347,15.32C10.523,15.496 10.74,15.584 11,15.584Z"
|
||||||
|
android:fillColor="#1B1D22"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M14.667,15.584C14.926,15.584 15.144,15.496 15.32,15.32C15.495,15.145 15.583,14.927 15.583,14.667V12.834C15.583,12.574 15.495,12.357 15.32,12.181C15.144,12.005 14.926,11.917 14.667,11.917C14.407,11.917 14.189,12.005 14.014,12.181C13.838,12.357 13.75,12.574 13.75,12.834V14.667C13.75,14.927 13.838,15.145 14.014,15.32C14.189,15.496 14.407,15.584 14.667,15.584Z"
|
||||||
|
android:fillColor="#1B1D22"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -44,4 +44,9 @@ enum class FeatureFlags(
|
||||||
// Do not forget to edit StaticFeatureFlagProvider when enabling the feature.
|
// Do not forget to edit StaticFeatureFlagProvider when enabling the feature.
|
||||||
defaultValue = false,
|
defaultValue = false,
|
||||||
),
|
),
|
||||||
|
RichTextEditor(
|
||||||
|
key = "feature.richtexteditor",
|
||||||
|
title = "Enable rich text editor",
|
||||||
|
defaultValue = true,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
||||||
FeatureFlags.LocationSharing -> true
|
FeatureFlags.LocationSharing -> true
|
||||||
FeatureFlags.Polls -> true
|
FeatureFlags.Polls -> true
|
||||||
FeatureFlags.NotificationSettings -> false
|
FeatureFlags.NotificationSettings -> false
|
||||||
|
FeatureFlags.RichTextEditor -> true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,11 @@ interface MatrixRoom : Closeable {
|
||||||
|
|
||||||
suspend fun userAvatarUrl(userId: UserId): Result<String?>
|
suspend fun userAvatarUrl(userId: UserId): Result<String?>
|
||||||
|
|
||||||
suspend fun sendMessage(body: String, htmlBody: String): Result<Unit>
|
suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit>
|
||||||
|
|
||||||
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit>
|
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit>
|
||||||
|
|
||||||
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit>
|
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit>
|
||||||
|
|
||||||
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ val oidcConfiguration: OidcConfiguration = OidcConfiguration(
|
||||||
logoUri = "https://element.io/mobile-icon.png",
|
logoUri = "https://element.io/mobile-icon.png",
|
||||||
tosUri = "https://element.io/acceptable-use-policy-terms",
|
tosUri = "https://element.io/acceptable-use-policy-terms",
|
||||||
policyUri = "https://element.io/privacy",
|
policyUri = "https://element.io/privacy",
|
||||||
|
contacts = listOf(
|
||||||
|
"support@element.io",
|
||||||
|
),
|
||||||
/**
|
/**
|
||||||
* Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
|
* Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,12 @@ import org.matrix.rustcomponents.sdk.RequiredState
|
||||||
import org.matrix.rustcomponents.sdk.Room
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||||
import org.matrix.rustcomponents.sdk.RoomMember
|
import org.matrix.rustcomponents.sdk.RoomMember
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
|
||||||
import org.matrix.rustcomponents.sdk.RoomSubscription
|
import org.matrix.rustcomponents.sdk.RoomSubscription
|
||||||
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
||||||
import org.matrix.rustcomponents.sdk.genTransactionId
|
import org.matrix.rustcomponents.sdk.genTransactionId
|
||||||
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
|
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
|
||||||
|
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
@ -227,32 +229,32 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendMessage(body: String, htmlBody: String): Result<Unit> = withContext(roomDispatcher) {
|
override suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
|
||||||
val transactionId = genTransactionId()
|
val transactionId = genTransactionId()
|
||||||
messageEventContentFromHtml(body, htmlBody).use { content ->
|
messageEventContentFromParts(body, htmlBody).use { content ->
|
||||||
runCatching {
|
runCatching {
|
||||||
innerRoom.send(content, transactionId)
|
innerRoom.send(content, transactionId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit> =
|
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit> =
|
||||||
withContext(roomDispatcher) {
|
withContext(roomDispatcher) {
|
||||||
if (originalEventId != null) {
|
if (originalEventId != null) {
|
||||||
runCatching {
|
runCatching {
|
||||||
innerRoom.edit(messageEventContentFromHtml(body, htmlBody), originalEventId.value, transactionId?.value)
|
innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value, transactionId?.value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runCatching {
|
runCatching {
|
||||||
transactionId?.let { cancelSend(it) }
|
transactionId?.let { cancelSend(it) }
|
||||||
innerRoom.send(messageEventContentFromHtml(body, htmlBody), genTransactionId())
|
innerRoom.send(messageEventContentFromParts(body, htmlBody), genTransactionId())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit> = withContext(roomDispatcher) {
|
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
innerRoom.sendReply(messageEventContentFromHtml(body, htmlBody), eventId.value, genTransactionId())
|
innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventId.value, genTransactionId())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -456,4 +458,11 @@ class RustMatrixRoom(
|
||||||
MediaUploadHandlerImpl(files, handle())
|
MediaUploadHandlerImpl(files, handle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation =
|
||||||
|
if(htmlBody != null) {
|
||||||
|
messageEventContentFromHtml(body, htmlBody)
|
||||||
|
} else {
|
||||||
|
messageEventContentFromMarkdown(body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class FakeMatrixRoom(
|
||||||
private var sendPollResponseResult = Result.success(Unit)
|
private var sendPollResponseResult = Result.success(Unit)
|
||||||
private var endPollResult = Result.success(Unit)
|
private var endPollResult = Result.success(Unit)
|
||||||
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
|
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
|
||||||
val editMessageCalls = mutableListOf<Pair<String, String>>()
|
val editMessageCalls = mutableListOf<Pair<String, String?>>()
|
||||||
|
|
||||||
var sendMediaCount = 0
|
var sendMediaCount = 0
|
||||||
private set
|
private set
|
||||||
|
|
@ -171,7 +171,7 @@ class FakeMatrixRoom(
|
||||||
userAvatarUrlResult
|
userAvatarUrlResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendMessage(body: String, htmlBody: String) = simulateLongTask {
|
override suspend fun sendMessage(body: String, htmlBody: String?) = simulateLongTask {
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,15 +200,15 @@ class FakeMatrixRoom(
|
||||||
return cancelSendResult
|
return cancelSendResult
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit> {
|
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit> {
|
||||||
editMessageCalls += body to htmlBody
|
editMessageCalls += body to htmlBody
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
var replyMessageParameter: Pair<String, String>? = null
|
var replyMessageParameter: Pair<String, String?>? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit> {
|
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> {
|
||||||
replyMessageParameter = body to htmlBody
|
replyMessageParameter = body to htmlBody
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,6 @@
|
||||||
package io.element.android.libraries.textcomposer
|
package io.element.android.libraries.textcomposer
|
||||||
|
|
||||||
data class Message(
|
data class Message(
|
||||||
val html: String,
|
val html: String?,
|
||||||
val markdown: String,
|
val markdown: String,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ fun TextComposer(
|
||||||
state: RichTextEditorState,
|
state: RichTextEditorState,
|
||||||
composerMode: MessageComposerMode,
|
composerMode: MessageComposerMode,
|
||||||
canSendMessage: Boolean,
|
canSendMessage: Boolean,
|
||||||
|
enableTextFormatting: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
showTextFormatting: Boolean = false,
|
showTextFormatting: Boolean = false,
|
||||||
onRequestFocus: () -> Unit = {},
|
onRequestFocus: () -> Unit = {},
|
||||||
|
|
@ -101,7 +102,8 @@ fun TextComposer(
|
||||||
onError: (Throwable) -> Unit = {},
|
onError: (Throwable) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val onSendClicked = {
|
val onSendClicked = {
|
||||||
onSendMessage(Message(html = state.messageHtml, markdown = state.messageMarkdown))
|
val html = if (enableTextFormatting) state.messageHtml else null
|
||||||
|
onSendMessage(Message(html = html, markdown = state.messageMarkdown))
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -600,6 +602,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
|
||||||
onSendMessage = {},
|
onSendMessage = {},
|
||||||
composerMode = MessageComposerMode.Normal(""),
|
composerMode = MessageComposerMode.Normal(""),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
TextComposer(
|
TextComposer(
|
||||||
RichTextEditorState("A message", fake = true).apply { requestFocus() },
|
RichTextEditorState("A message", fake = true).apply { requestFocus() },
|
||||||
|
|
@ -607,6 +610,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
|
||||||
onSendMessage = {},
|
onSendMessage = {},
|
||||||
composerMode = MessageComposerMode.Normal(""),
|
composerMode = MessageComposerMode.Normal(""),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
TextComposer(
|
TextComposer(
|
||||||
RichTextEditorState(
|
RichTextEditorState(
|
||||||
|
|
@ -619,6 +623,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
|
||||||
onSendMessage = {},
|
onSendMessage = {},
|
||||||
composerMode = MessageComposerMode.Normal(""),
|
composerMode = MessageComposerMode.Normal(""),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
TextComposer(
|
TextComposer(
|
||||||
RichTextEditorState("A message without focus", fake = true),
|
RichTextEditorState("A message without focus", fake = true),
|
||||||
|
|
@ -626,6 +631,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
|
||||||
onSendMessage = {},
|
onSendMessage = {},
|
||||||
composerMode = MessageComposerMode.Normal(""),
|
composerMode = MessageComposerMode.Normal(""),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -639,18 +645,21 @@ internal fun TextComposerFormattingPreview() = ElementPreview {
|
||||||
canSendMessage = false,
|
canSendMessage = false,
|
||||||
showTextFormatting = true,
|
showTextFormatting = true,
|
||||||
composerMode = MessageComposerMode.Normal(""),
|
composerMode = MessageComposerMode.Normal(""),
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
TextComposer(
|
TextComposer(
|
||||||
RichTextEditorState("A message", fake = true),
|
RichTextEditorState("A message", fake = true),
|
||||||
canSendMessage = true,
|
canSendMessage = true,
|
||||||
showTextFormatting = true,
|
showTextFormatting = true,
|
||||||
composerMode = MessageComposerMode.Normal(""),
|
composerMode = MessageComposerMode.Normal(""),
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
TextComposer(
|
TextComposer(
|
||||||
RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", fake = true),
|
RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", fake = true),
|
||||||
canSendMessage = true,
|
canSendMessage = true,
|
||||||
showTextFormatting = true,
|
showTextFormatting = true,
|
||||||
composerMode = MessageComposerMode.Normal(""),
|
composerMode = MessageComposerMode.Normal(""),
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -664,6 +673,7 @@ internal fun TextComposerEditPreview() = ElementPreview {
|
||||||
onSendMessage = {},
|
onSendMessage = {},
|
||||||
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")),
|
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -684,6 +694,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
||||||
"To preview larger textfields and long lines with overflow"
|
"To preview larger textfields and long lines with overflow"
|
||||||
),
|
),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
TextComposer(
|
TextComposer(
|
||||||
RichTextEditorState("A message", fake = true),
|
RichTextEditorState("A message", fake = true),
|
||||||
|
|
@ -701,6 +712,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
||||||
defaultContent = "image.jpg"
|
defaultContent = "image.jpg"
|
||||||
),
|
),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
TextComposer(
|
TextComposer(
|
||||||
RichTextEditorState("A message", fake = true),
|
RichTextEditorState("A message", fake = true),
|
||||||
|
|
@ -718,6 +730,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
||||||
defaultContent = "video.mp4"
|
defaultContent = "video.mp4"
|
||||||
),
|
),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
TextComposer(
|
TextComposer(
|
||||||
RichTextEditorState("A message", fake = true),
|
RichTextEditorState("A message", fake = true),
|
||||||
|
|
@ -735,6 +748,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
||||||
defaultContent = "logs.txt"
|
defaultContent = "logs.txt"
|
||||||
),
|
),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
TextComposer(
|
TextComposer(
|
||||||
RichTextEditorState("A message", fake = true).apply { requestFocus() },
|
RichTextEditorState("A message", fake = true).apply { requestFocus() },
|
||||||
|
|
@ -752,6 +766,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
||||||
defaultContent = "Shared location"
|
defaultContent = "Shared location"
|
||||||
),
|
),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
|
enableTextFormatting = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ee62a7f77e591d238878e72e3f2fbe489e8c4e8c6a5c121b16e120ad5c43836d
|
oid sha256:8421be9fe6518a4a610e561692352c28c63b1e588fae4f7617f6be94439852f2
|
||||||
size 44185
|
size 45078
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:20847fc7ab9e27171ae0ae37a95e7524aac0bd2479ff5b8b1130a5bc26dbb3e6
|
oid sha256:d6a15254ca331bc6d8ec6a6408b0a5a3d4fd26961e7c6025805ec7841a22e017
|
||||||
size 28232
|
size 28313
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d4bdf3944d30980e9bfd7cfffbabe2dac9861748cc3006e751bfd5cbf59c86db
|
oid sha256:4c30a6c0c7169044216a02dde5605d59fba9307499855ffc1fa0d3fcd1650f5e
|
||||||
size 42475
|
size 43291
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a62ebcf0024a1876984c936c29037cf04e2e02aefb3bea461030c092fbde2937
|
oid sha256:e0355effa8376bc88f9474f4d0e7f595708702f4fda5e39becf811fef10b0d16
|
||||||
size 26983
|
size 27081
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6c0485788c8776536a01613058ab001bad02a67519cf0cb4892505c7c3715fea
|
oid sha256:c40273c36eb1479f284e75fa91d4a75b1ae97edd0242dda37a2d4a8f10394928
|
||||||
size 144681
|
size 138700
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b4a259f036e555fdc52ecc8f0c9578dae70ed677235fbfd344bc31cbabc76be5
|
oid sha256:d7c47c713c74766c39d3b349d9e421ea588261e043105a5475cd8e68af782806
|
||||||
size 181478
|
size 185325
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d5966cae164fed6a3a12c1e1dd940ae7b0601e4ba69d11f49d18740bdab8c520
|
oid sha256:5d5479f3a6ea138b00c8f6a376cc4a67984385a087fb32bcdb764b2996e89402
|
||||||
size 130130
|
size 137137
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6c0485788c8776536a01613058ab001bad02a67519cf0cb4892505c7c3715fea
|
oid sha256:c40273c36eb1479f284e75fa91d4a75b1ae97edd0242dda37a2d4a8f10394928
|
||||||
size 144681
|
size 138700
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b4a259f036e555fdc52ecc8f0c9578dae70ed677235fbfd344bc31cbabc76be5
|
oid sha256:d7c47c713c74766c39d3b349d9e421ea588261e043105a5475cd8e68af782806
|
||||||
size 181478
|
size 185325
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b8cb21e08faf31779111d78c2be1e4e68e6f3bc994b16c108bd654f13d39fa02
|
oid sha256:5d5479f3a6ea138b00c8f6a376cc4a67984385a087fb32bcdb764b2996e89402
|
||||||
size 130130
|
size 137137
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:15d14bf99af3cd0433870ebd6032d9bf4a45196e2ef1df7184cc55859a704dee
|
oid sha256:339e5f22a47e29b6f681ac169be3b65568b7fa5d1197b0e81fb68bef639ae5c2
|
||||||
size 49008
|
size 49033
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6e47fc219bbd63b76d01a5e50c3e4c6b1b0a8b4ec40b08b13271d0b2673d8d5e
|
oid sha256:c439f506df4eaed4ad35148d1734630a89b0de3ccb097864b7ff853c679772c1
|
||||||
size 50932
|
size 50964
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cacb91ffca97f21bbc19b29525813f58fc8017a858aa28ccd4e620b70a8cd9ca
|
oid sha256:f85799360092c7e2d02f64f32668279c4e0c39bca3d45c9db13d503ddb7bf753
|
||||||
size 46119
|
size 46162
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3f1c14a23ee598ece6843a68d3ca0b1d1f725f53174bd493e27c6137de70c508
|
oid sha256:b05475d6509b15fcffeae54bdebf434a529a99af5173c48992677a88b654f3eb
|
||||||
size 48296
|
size 48336
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:367f76f13127a604d1fec6e86d56611b01765dc90462dd329cd567e55a52fdca
|
oid sha256:eb78a3bc85d7f9ead19ba7d2723b997af1a399167ce28004be8ccbf0dd8ce5fd
|
||||||
size 7827
|
size 7736
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a14f96fc1119481ca8e7336ac0a65cb831a2bfff3e8e43270b0da8844f419f0e
|
oid sha256:499df8785b50d0bd1d5466b8a5b65ab1a8eb358f0c1341ad5956f5d1931859ed
|
||||||
size 7982
|
size 7987
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c9caf6d9b49cf7eeaa41025617a7cc5b1d89bbd7468f7ecbfc0cac73ca4a04cc
|
oid sha256:021d867d665e0fa77aa10a85e734e4a9416b96d627b21ce462f0e327c5e58333
|
||||||
size 7452
|
size 7392
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f2782edd31f68b7aa27e0c35b29d7c84574326cb56c65394738357479d408f5f
|
oid sha256:06617cda0f93ce0ea26b2a77a243371cdaf4f9a2956d8159ecb7196f7f6fe082
|
||||||
size 144667
|
size 139282
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cd352f748e3fd08225aa714181fdc18d53e91a8c62f8e5e860b67ff5aaeb7b8e
|
oid sha256:55d2d11ba729a68b1b62e9a69c3308594839fdb3612ff3db59850f0b346c28da
|
||||||
size 181969
|
size 186118
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:43da903ec671dd1cb591396306ce8c057432ffa8c1bfce7a509f28576e82b530
|
oid sha256:62c57a45c2cb1aac23fa0c3af44f90a5d7b9a1e1626ec49eb94a2c126f53f96d
|
||||||
size 130337
|
size 137535
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f2782edd31f68b7aa27e0c35b29d7c84574326cb56c65394738357479d408f5f
|
oid sha256:06617cda0f93ce0ea26b2a77a243371cdaf4f9a2956d8159ecb7196f7f6fe082
|
||||||
size 144667
|
size 139282
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cd352f748e3fd08225aa714181fdc18d53e91a8c62f8e5e860b67ff5aaeb7b8e
|
oid sha256:55d2d11ba729a68b1b62e9a69c3308594839fdb3612ff3db59850f0b346c28da
|
||||||
size 181969
|
size 186118
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8141eaa257454258d2926df5ee970daf5ee165b774e4b948256b90e22a388d4c
|
oid sha256:62c57a45c2cb1aac23fa0c3af44f90a5d7b9a1e1626ec49eb94a2c126f53f96d
|
||||||
size 130337
|
size 137535
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f95fbf1b51966c1c6b7b149c771f33400455d3c999b505e1aa65f0e3eb41c0ef
|
oid sha256:f0ad6895582183b697732ef032f89a6a0cad32106c9bbf30925c390dcab65ee6
|
||||||
size 140928
|
size 151752
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e8d522eb99079792d010e053d488f78a95e91f5a1b614b9d512bd005f0df81cf
|
oid sha256:676b62ab782652c45a7267aac5df12943fd181bea48964d28dce664884ae4182
|
||||||
size 146976
|
size 156619
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c196f9cb2da2366e4e67d59012f61c5f50a5775475230c3a0cbc8d823ba53dcd
|
oid sha256:f6010f8c84263cad47587a20c9d7da64aef2a8520c01765ee4966e4b6bdd8327
|
||||||
size 63317
|
size 63371
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:59773d8a5435a37b3d47bf0c41412299b0b277af4be7a1a25ccb33b4b60a32db
|
oid sha256:6e12464ce600a1cd6ca6ea9dd930c2235e033896b336109e094b84dcf2d4e624
|
||||||
size 65397
|
size 65459
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ee692fa3f3dbe4500cff084526a4537fef6444440c78f63a03d990a6a027f794
|
oid sha256:e74d57cb6f5cda78086989289d0f0543cfd3747e1cebec0bd656bd5be2d8bac1
|
||||||
size 69844
|
size 69944
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0b8155faa2599615048193667f540664c7185ac7b6a9159afdc14dfb6286d62e
|
oid sha256:9baf1ea5fca980be4744655283bd61de650c9f8399a9f3441a9a3f9c5f43566e
|
||||||
size 71826
|
size 71852
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:771343ea6a9d83160284b3a9248f744c68ab25c2ad64817269c3628a5ca67972
|
oid sha256:343d84dc714bb672084362155362b04936ae491f437e492332bff553d7776078
|
||||||
size 62319
|
size 62478
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4663107c550f193aa9bb23b6bccf9229aed5ad30c380ce6b9a326e6a3fe5def7
|
oid sha256:daee8f18d24bcc07cf8edd9b902b03c3fb406199a925606b1caf34cee08ff9a1
|
||||||
size 64854
|
size 65076
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ca51c27cb553709458343bf16d8402a55eb1b7187b4b85191c3318f5b88c29db
|
oid sha256:f0cb7b8bd23784f2323ab61baad1a910b343fc498ea61f7364edeff9111992d0
|
||||||
size 69312
|
size 69478
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b0a825d8090414c617780bf2d7332b8e80e5e4de066cd117c53f5e90cae8d4f6
|
oid sha256:54a52c6de5fd501e5c4f260343dd9ec582b0a1dd094a18f43f2fc4aa2c8acfcc
|
||||||
size 72112
|
size 72145
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:eebf95f938f483da9b087eda86ae4a1baac594bc4b6f1f893d869b759679526c
|
oid sha256:b991a34a137d82ff4dedf48316a05ebbc008233984b11af70e64889a5fb5eda8
|
||||||
size 123267
|
size 127438
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4875df98092a8ef8bd1c2fda8c4a038ae2830b9ffa57e4ab71859523f97c26c9
|
oid sha256:43b6514716d18382c8962520928d9d2ff6d0f665b2471e8a24e5d2e44a25c88c
|
||||||
size 128062
|
size 132295
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fe0a95b6c94668709c75052feec2953d310b8bcfda42758b2898e28b384ce193
|
oid sha256:bdcc950b0ce924a73ae742f0bb3fc7279de46f36ba01d377822a2a14b6f90343
|
||||||
size 56189
|
size 56239
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:30119e8fcb65e8cbf058191268f0d70633a62b160b9f49f71879272d0d076537
|
oid sha256:0d7bc773e70c57226840b30675f14a7fd99026100d61f48eeab64fb55d43f7c6
|
||||||
size 241818
|
size 230426
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:73ca2b85c0f1f8f13cd3a0aae9d394116a460aa2c511ca1f2672a356eee496d2
|
oid sha256:87f93b06f658352e7d97617c28dc0a4bd7454263fd402abfc2676f548c41d6b2
|
||||||
size 241553
|
size 231314
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ff7953fabf75b346e61bd4335f3753331b498dbe5f17f9af3aad8a6a1ce051cd
|
oid sha256:f13fbb9bc776fa230b69c41e3faab659c52083d4e4f737f84b472c966f42c8dd
|
||||||
size 239490
|
size 229746
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e262b1128df681c9a98bbefe426a891d02e0500646319f2e1756c1b5c93f2d23
|
oid sha256:d312c80758e940b368e0b9eb2d41efb55c158691164bfde3456d7540080e46a9
|
||||||
size 239303
|
size 230651
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:15d14bf99af3cd0433870ebd6032d9bf4a45196e2ef1df7184cc55859a704dee
|
oid sha256:339e5f22a47e29b6f681ac169be3b65568b7fa5d1197b0e81fb68bef639ae5c2
|
||||||
size 49008
|
size 49033
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cacb91ffca97f21bbc19b29525813f58fc8017a858aa28ccd4e620b70a8cd9ca
|
oid sha256:f85799360092c7e2d02f64f32668279c4e0c39bca3d45c9db13d503ddb7bf753
|
||||||
size 46119
|
size 46162
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:434c4f6dc4ee4c66291eca2ff1a8ab2471aaf521dbbf0d32f53f277ea6519c07
|
oid sha256:85fbaa916a38c9b75a527dc3fddded9e9d06a98ab20f9f3eb74596c24ba4b5b7
|
||||||
size 49107
|
size 49105
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:63685437c43543115e8f509919ef179d70a620c012c8a46ebf6682dcfe9820c6
|
oid sha256:cee9e96cfefa6f84fb924857a92fd4ba04bbd196d94a6345bb821ff769fbd56d
|
||||||
size 45890
|
size 45889
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2264139761f7fe3977ece3c9a7d949ac51223dd209497b7b311987cba3e5a069
|
oid sha256:53cffdc5ea12b16db3f87ab12c28fede82ce5a744c222c9f19e5c35ef751c583
|
||||||
size 47154
|
size 47179
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f3ff35998ce8558b5a7af84e378bee039e36099084b357fffccf329a4983b035
|
oid sha256:96d696014b24a2a222ba331d754d6048063a9ec573158edc41ded92ed588b60d
|
||||||
size 43551
|
size 43593
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:51931b01aa27677c8d29a79bf92f4e8b48fc2be05e80fdabedfb9026dcbd8899
|
oid sha256:b967a0dc3bd9a466790503dd49c2fff6ce70156b51603022916aa9db3fc5c372
|
||||||
size 31634
|
size 29976
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5441c89fc077720b52479d4daf1ebd5fe5b98b63f35141a550dac228fe1c649f
|
oid sha256:8ec89c6b12c4eb1f8059d9d0d183f6635f5647393134c858720f3ef72b8c4c36
|
||||||
size 10617
|
size 36837
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9156202a9f621b75cff3ca45aac33d72a938620049948828fc34aab3b9e0980d
|
oid sha256:71c2ac04d7fb904b648fc383f430e3b8a4003e93e5964b60eb545248186cd8dc
|
||||||
size 55382
|
size 53912
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0c7ee3acbf4653e49633e68f896fe0f5f73591c2cbc889204cc244b319996e0d
|
oid sha256:3277b51e1a7153487e2d9764d5c495419f8df7fa635574302237ebf3549a4b3c
|
||||||
size 78348
|
size 76821
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9156202a9f621b75cff3ca45aac33d72a938620049948828fc34aab3b9e0980d
|
oid sha256:71c2ac04d7fb904b648fc383f430e3b8a4003e93e5964b60eb545248186cd8dc
|
||||||
size 55382
|
size 53912
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:55800431dfe4453fa3c11f97bfae53ceaf95f1c18b72bcc8620b8c3295b26b27
|
oid sha256:0104fa9aca0b3d25f06225f54529e88d78f7d42aabd102b2c065c774427bbf3b
|
||||||
size 56388
|
size 55038
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b2d288f46eb76b8a71a8c1a2ec60ba7c3b8b1bf2a92be9d8daf33c06b3a0f126
|
oid sha256:c2bf9d1733fc7230816cbcfbcef6780d34e687a064c289f2f9d651cf3047a1bd
|
||||||
size 56593
|
size 55107
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c0e5bc6abe96bff50960c5ace2c4eccd5aabdd9f3ccd015fae867b4c3bc2b423
|
oid sha256:a3cdb5de68791a0e5425ff9a29eaa8c1dc09aea9448211b9ad32f136bf11ace7
|
||||||
size 56902
|
size 55418
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1f385e70f3a8bda7c0c2f7c090ba665575239299472c0fbd62f2821783ceb065
|
oid sha256:3a9958e430c07445b9dcdc31a483c7925e2bd2a0d3fbbaf1ee5482051e2afe51
|
||||||
size 39016
|
size 65488
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4bdb22d629e4bc3273aac4944802d1c91b43065858f4f4cc90052bd78ae6dd89
|
oid sha256:c238218a16f7dc5c25125177434a8397821d5881971dd0404b9d79b7b181a592
|
||||||
size 62998
|
size 89294
|
||||||
|
|
|
||||||
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