Merge branch 'develop' into feature/fga/create_room_improve_address
5
.github/workflows/build.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [debug, release, nightly, samples]
|
||||
variant: [debug, release, nightly]
|
||||
fail-fast: false
|
||||
# Allow all jobs on develop. Just one per PR.
|
||||
concurrency:
|
||||
@@ -82,6 +82,3 @@ jobs:
|
||||
- name: Compile nightly sources
|
||||
if: ${{ matrix.variant == 'nightly' }}
|
||||
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Compile samples minimal
|
||||
if: ${{ matrix.variant == 'samples' }}
|
||||
run: ./gradlew :samples:minimal:assemble $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
@@ -49,8 +49,6 @@ Please ensure that you're using the project formatting rules (which are in the p
|
||||
|
||||
This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`.
|
||||
|
||||
Note: please make sure that the configuration is `app` and not `samples.minimal`.
|
||||
|
||||
## Strings
|
||||
|
||||
The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with Element X iOS.
|
||||
|
||||
@@ -40,7 +40,7 @@ We want:
|
||||
|
||||
The CI checks that:
|
||||
|
||||
1. The code is compiling, without any warnings, for all the app build types and variants and for the minimal app
|
||||
1. The code is compiling, without any warnings, for all the app build types and variants
|
||||
2. The tests are passing
|
||||
3. The code quality is good (detekt, ktlint, lint)
|
||||
4. The code is running and smoke tests are passing (maestro)
|
||||
|
||||
@@ -212,7 +212,7 @@ class MessagesNode @AssistedInject constructor(
|
||||
state = state,
|
||||
onBackClick = this::navigateUp,
|
||||
onRoomDetailsClick = this::onRoomDetailsClick,
|
||||
onEventClick = this::onEventClick,
|
||||
onEventContentClick = this::onEventClick,
|
||||
onPreviewAttachments = this::onPreviewAttachments,
|
||||
onUserDataClick = this::onUserDataClick,
|
||||
onLinkClick = { url -> onLinkClick(activity, isDark, url, state.timelineState.eventSink) },
|
||||
|
||||
@@ -114,7 +114,7 @@ fun MessagesView(
|
||||
state: MessagesState,
|
||||
onBackClick: () -> Unit,
|
||||
onRoomDetailsClick: () -> Unit,
|
||||
onEventClick: (event: TimelineItem.Event) -> Boolean,
|
||||
onEventContentClick: (event: TimelineItem.Event) -> Boolean,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onLinkClick: (String) -> Unit,
|
||||
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
|
||||
@@ -142,9 +142,9 @@ fun MessagesView(
|
||||
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose
|
||||
val localView = LocalView.current
|
||||
|
||||
fun onMessageClick(event: TimelineItem.Event) {
|
||||
fun onContentClick(event: TimelineItem.Event) {
|
||||
Timber.v("onMessageClick= ${event.id}")
|
||||
val hideKeyboard = onEventClick(event)
|
||||
val hideKeyboard = onEventContentClick(event)
|
||||
if (hideKeyboard) {
|
||||
localView.hideKeyboard()
|
||||
}
|
||||
@@ -206,7 +206,7 @@ fun MessagesView(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
onMessageClick = ::onMessageClick,
|
||||
onContentClick = ::onContentClick,
|
||||
onMessageLongClick = ::onMessageLongClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
@@ -306,7 +306,7 @@ private fun AttachmentStateView(
|
||||
@Composable
|
||||
private fun MessagesViewContent(
|
||||
state: MessagesState,
|
||||
onMessageClick: (TimelineItem.Event) -> Unit,
|
||||
onContentClick: (TimelineItem.Event) -> Unit,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onLinkClick: (String) -> Unit,
|
||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||
@@ -382,7 +382,7 @@ private fun MessagesViewContent(
|
||||
timelineProtectionState = state.timelineProtectionState,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onMessageClick = onMessageClick,
|
||||
onContentClick = onContentClick,
|
||||
onMessageLongClick = onMessageLongClick,
|
||||
onSwipeToReply = onSwipeToReply,
|
||||
onReactionClick = onReactionClick,
|
||||
@@ -568,7 +568,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onRoomDetailsClick = {},
|
||||
onEventClick = { false },
|
||||
onEventContentClick = { false },
|
||||
onUserDataClick = {},
|
||||
onLinkClick = {},
|
||||
onPreviewAttachments = {},
|
||||
|
||||
@@ -33,7 +33,7 @@ internal fun MessagesViewWithIdentityChangePreview(
|
||||
),
|
||||
onBackClick = {},
|
||||
onRoomDetailsClick = {},
|
||||
onEventClick = { false },
|
||||
onEventContentClick = { false },
|
||||
onUserDataClick = {},
|
||||
onLinkClick = {},
|
||||
onPreviewAttachments = {},
|
||||
|
||||
@@ -216,7 +216,7 @@ private fun PinnedMessagesListLoaded(
|
||||
focusedEventId = null,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onClick = onEventClick,
|
||||
onContentClick = onEventClick,
|
||||
onLongClick = ::onMessageLongClick,
|
||||
inReplyToClick = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
@@ -230,6 +230,7 @@ private fun PinnedMessagesListLoaded(
|
||||
TimelineItemEventContentViewWrapper(
|
||||
event = event,
|
||||
timelineProtectionState = state.timelineProtectionState,
|
||||
onContentClick = { onEventClick(event) },
|
||||
onLinkClick = onLinkClick,
|
||||
modifier = contentModifier,
|
||||
onContentLayoutChange = onContentLayoutChange
|
||||
@@ -244,6 +245,7 @@ private fun PinnedMessagesListLoaded(
|
||||
private fun TimelineItemEventContentViewWrapper(
|
||||
event: TimelineItem.Event,
|
||||
timelineProtectionState: TimelineProtectionState,
|
||||
onContentClick: () -> Unit,
|
||||
onLinkClick: (String) -> Unit,
|
||||
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -258,10 +260,11 @@ private fun TimelineItemEventContentViewWrapper(
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
|
||||
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||
onLinkClick = onLinkClick,
|
||||
eventSink = { },
|
||||
modifier = modifier,
|
||||
onContentClick = onContentClick,
|
||||
onContentLayoutChange = onContentLayoutChange
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ fun TimelineView(
|
||||
timelineProtectionState: TimelineProtectionState,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onLinkClick: (String) -> Unit,
|
||||
onMessageClick: (TimelineItem.Event) -> Unit,
|
||||
onContentClick: (TimelineItem.Event) -> Unit,
|
||||
onMessageLongClick: (TimelineItem.Event) -> Unit,
|
||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit,
|
||||
@@ -141,7 +141,7 @@ fun TimelineView(
|
||||
focusedEventId = state.focusedEventId,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onClick = onMessageClick,
|
||||
onContentClick = onContentClick,
|
||||
onLongClick = onMessageLongClick,
|
||||
inReplyToClick = ::inReplyToClick,
|
||||
onReactionClick = onReactionClick,
|
||||
@@ -322,7 +322,7 @@ internal fun TimelineViewPreview(
|
||||
timelineProtectionState = aTimelineProtectionState(),
|
||||
onUserDataClick = {},
|
||||
onLinkClick = {},
|
||||
onMessageClick = {},
|
||||
onContentClick = {},
|
||||
onMessageLongClick = {},
|
||||
onSwipeToReply = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
|
||||
@@ -41,7 +41,7 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview {
|
||||
timelineProtectionState = aTimelineProtectionState(),
|
||||
onUserDataClick = {},
|
||||
onLinkClick = {},
|
||||
onMessageClick = {},
|
||||
onContentClick = {},
|
||||
onMessageLongClick = {},
|
||||
onSwipeToReply = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
|
||||
@@ -30,7 +30,7 @@ internal fun ATimelineItemEventRow(
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
isHighlighted = isHighlighted,
|
||||
onClick = {},
|
||||
onContentClick = {},
|
||||
onLongClick = {},
|
||||
onLinkClick = {},
|
||||
onUserDataClick = {},
|
||||
|
||||
@@ -114,7 +114,7 @@ fun TimelineItemEventRow(
|
||||
renderReadReceipts: Boolean,
|
||||
isLastOutgoingMessage: Boolean,
|
||||
isHighlighted: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onContentClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
onLinkClick: (String) -> Unit,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
@@ -130,7 +130,8 @@ fun TimelineItemEventRow(
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
|
||||
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||
onContentClick = onContentClick,
|
||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||
onLinkClick = onLinkClick,
|
||||
eventSink = eventSink,
|
||||
modifier = contentModifier,
|
||||
@@ -150,6 +151,12 @@ fun TimelineItemEventRow(
|
||||
inReplyToClick(inReplyToEventId)
|
||||
}
|
||||
|
||||
val onWholeItemClick = if (event.isWholeContentClickable) {
|
||||
onContentClick
|
||||
} else {
|
||||
{}
|
||||
}
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
if (event.groupPosition.isNew()) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -173,7 +180,7 @@ fun TimelineItemEventRow(
|
||||
isHighlighted = isHighlighted,
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onContentClick = onWholeItemClick,
|
||||
onLongClick = onLongClick,
|
||||
inReplyToClick = ::inReplyToClick,
|
||||
onUserDataClick = ::onUserDataClick,
|
||||
@@ -207,7 +214,7 @@ fun TimelineItemEventRow(
|
||||
isHighlighted = isHighlighted,
|
||||
timelineRoomInfo = timelineRoomInfo,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onContentClick = onWholeItemClick,
|
||||
onLongClick = onLongClick,
|
||||
inReplyToClick = ::inReplyToClick,
|
||||
onUserDataClick = ::onUserDataClick,
|
||||
@@ -263,7 +270,7 @@ private fun TimelineItemEventRowContent(
|
||||
isHighlighted: Boolean,
|
||||
timelineRoomInfo: TimelineRoomInfo,
|
||||
interactionSource: MutableInteractionSource,
|
||||
onClick: () -> Unit,
|
||||
onContentClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
inReplyToClick: () -> Unit,
|
||||
onUserDataClick: () -> Unit,
|
||||
@@ -340,7 +347,7 @@ private fun TimelineItemEventRowContent(
|
||||
},
|
||||
state = bubbleState,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onClick = onContentClick,
|
||||
onLongClick = onLongClick,
|
||||
) {
|
||||
MessageEventBubbleContent(
|
||||
|
||||
@@ -57,10 +57,11 @@ fun TimelineItemGroupedEventsRow(
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
|
||||
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||
onLinkClick = onLinkClick,
|
||||
eventSink = eventSink,
|
||||
modifier = contentModifier,
|
||||
onContentClick = {},
|
||||
onContentLayoutChange = onContentLayoutChange
|
||||
)
|
||||
},
|
||||
@@ -121,10 +122,11 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
|
||||
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||
onLinkClick = onLinkClick,
|
||||
eventSink = eventSink,
|
||||
modifier = contentModifier,
|
||||
onContentClick = {},
|
||||
onContentLayoutChange = onContentLayoutChange
|
||||
)
|
||||
},
|
||||
@@ -152,7 +154,7 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||
focusedEventId = focusedEventId,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onClick = onClick,
|
||||
onContentClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
inReplyToClick = inReplyToClick,
|
||||
onReactionClick = onReactionClick,
|
||||
|
||||
@@ -28,7 +28,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||
import io.element.android.features.messages.impl.timeline.protection.mustBeProtected
|
||||
import io.element.android.libraries.designsystem.text.toPx
|
||||
import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
@@ -44,7 +43,7 @@ internal fun TimelineItemRow(
|
||||
focusedEventId: EventId?,
|
||||
onUserDataClick: (UserId) -> Unit,
|
||||
onLinkClick: (String) -> Unit,
|
||||
onClick: (TimelineItem.Event) -> Unit,
|
||||
onContentClick: (TimelineItem.Event) -> Unit,
|
||||
onLongClick: (TimelineItem.Event) -> Unit,
|
||||
inReplyToClick: (EventId) -> Unit,
|
||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||
@@ -60,7 +59,8 @@ internal fun TimelineItemRow(
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
|
||||
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
||||
onContentClick = { onContentClick(event) },
|
||||
onLinkClick = onLinkClick,
|
||||
eventSink = eventSink,
|
||||
modifier = contentModifier,
|
||||
@@ -95,7 +95,7 @@ internal fun TimelineItemRow(
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
isHighlighted = timelineItem.isEvent(focusedEventId),
|
||||
onClick = { onClick(timelineItem) },
|
||||
onClick = { onContentClick(timelineItem) },
|
||||
onReadReceiptsClick = onReadReceiptClick,
|
||||
onLongClick = { onLongClick(timelineItem) },
|
||||
eventSink = eventSink,
|
||||
@@ -118,11 +118,7 @@ internal fun TimelineItemRow(
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
isHighlighted = timelineItem.isEvent(focusedEventId),
|
||||
onClick = if (timelineProtectionState.hideMediaContent(timelineItem.eventId) && timelineItem.mustBeProtected()) {
|
||||
{}
|
||||
} else {
|
||||
{ onClick(timelineItem) }
|
||||
},
|
||||
onContentClick = { onContentClick(timelineItem) },
|
||||
onLongClick = { onLongClick(timelineItem) },
|
||||
onLinkClick = onLinkClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
@@ -148,7 +144,7 @@ internal fun TimelineItemRow(
|
||||
renderReadReceipts = renderReadReceipts,
|
||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||
focusedEventId = focusedEventId,
|
||||
onClick = onClick,
|
||||
onClick = onContentClick,
|
||||
onLongClick = onLongClick,
|
||||
inReplyToClick = inReplyToClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
|
||||
@@ -72,8 +72,9 @@ fun TimelineItemStateEventRow(
|
||||
content = event.content,
|
||||
onLinkClick = {},
|
||||
hideMediaContent = false,
|
||||
onShowClick = {},
|
||||
onShowContentClick = {},
|
||||
eventSink = eventSink,
|
||||
onContentClick = {},
|
||||
modifier = Modifier.defaultTimelineContentPadding()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ import io.element.android.libraries.architecture.Presenter
|
||||
fun TimelineItemEventContentView(
|
||||
content: TimelineItemEventContent,
|
||||
hideMediaContent: Boolean,
|
||||
onShowClick: () -> Unit,
|
||||
onContentClick: () -> Unit,
|
||||
onShowContentClick: () -> Unit,
|
||||
onLinkClick: (url: String) -> Unit,
|
||||
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -67,25 +68,31 @@ fun TimelineItemEventContentView(
|
||||
)
|
||||
is TimelineItemLocationContent -> TimelineItemLocationView(
|
||||
content = content,
|
||||
onContentClick = onContentClick,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemImageContent -> TimelineItemImageView(
|
||||
content = content,
|
||||
hideMediaContent = hideMediaContent,
|
||||
onShowClick = onShowClick,
|
||||
onContentClick = onContentClick,
|
||||
onShowContentClick = onShowContentClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onContentLayoutChange = onContentLayoutChange,
|
||||
modifier = modifier,
|
||||
)
|
||||
is TimelineItemStickerContent -> TimelineItemStickerView(
|
||||
content = content,
|
||||
hideMediaContent = hideMediaContent,
|
||||
onShowClick = onShowClick,
|
||||
onContentClick = onContentClick,
|
||||
onShowClick = onShowContentClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
is TimelineItemVideoContent -> TimelineItemVideoView(
|
||||
content = content,
|
||||
hideMediaContent = hideMediaContent,
|
||||
onShowClick = onShowClick,
|
||||
onContentClick = onContentClick,
|
||||
onShowContentClick = onShowContentClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onContentLayoutChange = onContentLayoutChange,
|
||||
modifier = modifier
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import android.text.SpannedString
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -50,7 +51,6 @@ import io.element.android.features.messages.impl.timeline.protection.ProtectedVi
|
||||
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
@@ -59,7 +59,9 @@ import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
fun TimelineItemImageView(
|
||||
content: TimelineItemImageContent,
|
||||
hideMediaContent: Boolean,
|
||||
onShowClick: () -> Unit,
|
||||
onContentClick: () -> Unit,
|
||||
onLinkClick: (String) -> Unit,
|
||||
onShowContentClick: () -> Unit,
|
||||
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -78,13 +80,14 @@ fun TimelineItemImageView(
|
||||
) {
|
||||
ProtectedView(
|
||||
hideContent = hideMediaContent,
|
||||
onShowClick = onShowClick,
|
||||
onShowClick = onShowContentClick,
|
||||
) {
|
||||
var isLoaded by remember { mutableStateOf(false) }
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier)
|
||||
.clickable(onClick = onContentClick),
|
||||
model = content.thumbnailMediaRequestData,
|
||||
contentScale = ContentScale.Fit,
|
||||
alignment = Alignment.Center,
|
||||
@@ -99,9 +102,7 @@ fun TimelineItemImageView(
|
||||
val caption = if (LocalInspectionMode.current) {
|
||||
SpannedString(content.caption)
|
||||
} else {
|
||||
content.formattedCaption?.body
|
||||
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
|
||||
?: SpannedString(content.caption)
|
||||
content.formattedCaption ?: SpannedString(content.caption)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||
@@ -114,6 +115,7 @@ fun TimelineItemImageView(
|
||||
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
|
||||
text = caption,
|
||||
style = ElementRichTextEditorStyle.textStyle(),
|
||||
onLinkClickedListener = onLinkClick,
|
||||
releaseOnDetach = false,
|
||||
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange),
|
||||
)
|
||||
@@ -128,7 +130,9 @@ internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageCon
|
||||
TimelineItemImageView(
|
||||
content = content,
|
||||
hideMediaContent = false,
|
||||
onShowClick = {},
|
||||
onShowContentClick = {},
|
||||
onContentClick = {},
|
||||
onLinkClick = {},
|
||||
onContentLayoutChange = {},
|
||||
)
|
||||
}
|
||||
@@ -139,7 +143,9 @@ internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview {
|
||||
TimelineItemImageView(
|
||||
content = aTimelineItemImageContent(),
|
||||
hideMediaContent = true,
|
||||
onShowClick = {},
|
||||
onShowContentClick = {},
|
||||
onContentClick = {},
|
||||
onLinkClick = {},
|
||||
onContentLayoutChange = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
@@ -25,9 +26,10 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||
@Composable
|
||||
fun TimelineItemLocationView(
|
||||
content: TimelineItemLocationContent,
|
||||
onContentClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
Column(modifier = modifier.clickable(onClick = onContentClick).fillMaxWidth()) {
|
||||
content.description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
@@ -51,5 +53,8 @@ fun TimelineItemLocationView(
|
||||
@Composable
|
||||
internal fun TimelineItemLocationViewPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) =
|
||||
ElementPreview {
|
||||
TimelineItemLocationView(content)
|
||||
TimelineItemLocationView(
|
||||
content = content,
|
||||
onContentClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -40,6 +41,7 @@ private const val STICKER_SIZE_IN_DP = 128
|
||||
fun TimelineItemStickerView(
|
||||
content: TimelineItemStickerContent,
|
||||
hideMediaContent: Boolean,
|
||||
onContentClick: () -> Unit,
|
||||
onShowClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -61,7 +63,8 @@ fun TimelineItemStickerView(
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier)
|
||||
.clickable(onClick = onContentClick),
|
||||
model = MediaRequestData(
|
||||
source = content.preferredMediaSource,
|
||||
kind = MediaRequestData.Kind.File(
|
||||
@@ -85,6 +88,7 @@ internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemSticke
|
||||
TimelineItemStickerView(
|
||||
content = content,
|
||||
hideMediaContent = false,
|
||||
onContentClick = {},
|
||||
onShowClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.components.event
|
||||
import android.text.SpannedString
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -56,7 +57,6 @@ import io.element.android.libraries.designsystem.components.blurhash.blurHashBac
|
||||
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT
|
||||
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
@@ -68,7 +68,9 @@ import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
fun TimelineItemVideoView(
|
||||
content: TimelineItemVideoContent,
|
||||
hideMediaContent: Boolean,
|
||||
onShowClick: () -> Unit,
|
||||
onContentClick: () -> Unit,
|
||||
onShowContentClick: () -> Unit,
|
||||
onLinkClick: (String) -> Unit,
|
||||
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -90,13 +92,14 @@ fun TimelineItemVideoView(
|
||||
) {
|
||||
ProtectedView(
|
||||
hideContent = hideMediaContent,
|
||||
onShowClick = onShowClick,
|
||||
onShowClick = onShowContentClick,
|
||||
) {
|
||||
var isLoaded by remember { mutableStateOf(false) }
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
|
||||
.then(if (isLoaded) Modifier.background(Color.White) else Modifier)
|
||||
.clickable(onClick = onContentClick),
|
||||
model = MediaRequestData(
|
||||
source = content.thumbnailSource,
|
||||
kind = MediaRequestData.Kind.Thumbnail(
|
||||
@@ -128,9 +131,7 @@ fun TimelineItemVideoView(
|
||||
val caption = if (LocalInspectionMode.current) {
|
||||
SpannedString(content.caption)
|
||||
} else {
|
||||
content.formattedCaption?.body
|
||||
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
|
||||
?: SpannedString(content.caption)
|
||||
content.formattedCaption ?: SpannedString(content.caption)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||
@@ -142,6 +143,7 @@ fun TimelineItemVideoView(
|
||||
.padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout
|
||||
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
|
||||
text = caption,
|
||||
onLinkClickedListener = onLinkClick,
|
||||
style = ElementRichTextEditorStyle.textStyle(),
|
||||
releaseOnDetach = false,
|
||||
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange),
|
||||
@@ -157,7 +159,9 @@ internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoCon
|
||||
TimelineItemVideoView(
|
||||
content = content,
|
||||
hideMediaContent = false,
|
||||
onShowClick = {},
|
||||
onShowContentClick = {},
|
||||
onContentClick = {},
|
||||
onLinkClick = {},
|
||||
onContentLayoutChange = {},
|
||||
)
|
||||
}
|
||||
@@ -168,7 +172,9 @@ internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview {
|
||||
TimelineItemVideoView(
|
||||
content = aTimelineItemVideoContent(),
|
||||
hideMediaContent = true,
|
||||
onShowClick = {},
|
||||
onShowContentClick = {},
|
||||
onContentClick = {},
|
||||
onLinkClick = {},
|
||||
onContentLayoutChange = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
TimelineItemImageContent(
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -105,7 +105,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
TimelineItemStickerContent(
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -142,7 +142,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
TimelineItemVideoContent(
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
videoSource = messageType.source,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -161,7 +161,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
TimelineItemAudioContent(
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -176,7 +176,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
eventId = eventId,
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -187,7 +187,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
TimelineItemAudioContent(
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
|
||||
@@ -202,7 +202,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
TimelineItemFileContent(
|
||||
filename = messageType.filename,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = messageType.formattedCaption,
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
fileSource = messageType.source,
|
||||
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),
|
||||
|
||||
@@ -9,8 +9,10 @@ package io.element.android.features.messages.impl.timeline.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
@@ -93,6 +95,17 @@ sealed interface TimelineItem {
|
||||
|
||||
val isRemote = eventId != null
|
||||
|
||||
/** Whether a click on any part of the event bubble should trigger the 'onContentClick' callback.
|
||||
*
|
||||
* This is `true` for all events except for visual media events with a caption or formatted caption.
|
||||
*/
|
||||
val isWholeContentClickable = when (content) {
|
||||
is TimelineItemStickerContent -> content.formattedCaption == null && content.caption == null
|
||||
is TimelineItemImageContent -> content.formattedCaption == null && content.caption == null
|
||||
is TimelineItemVideoContent -> content.formattedCaption == null && content.caption == null
|
||||
else -> true
|
||||
}
|
||||
|
||||
val eventOrTransactionId: EventOrTransactionId
|
||||
get() = EventOrTransactionId.from(eventId = eventId, transactionId = transactionId)
|
||||
|
||||
|
||||
@@ -8,14 +8,13 @@
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class TimelineItemAudioContent(
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
val duration: Duration,
|
||||
val mediaSource: MediaSource,
|
||||
val mimeType: String,
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
|
||||
@Immutable
|
||||
sealed interface TimelineItemEventContent {
|
||||
@@ -19,7 +18,7 @@ sealed interface TimelineItemEventContent {
|
||||
sealed interface TimelineItemEventContentWithAttachment : TimelineItemEventContent {
|
||||
val filename: String
|
||||
val caption: String?
|
||||
val formattedCaption: FormattedBody?
|
||||
val formattedCaption: CharSequence?
|
||||
|
||||
val bestDescription: String
|
||||
get() = caption ?: filename
|
||||
|
||||
@@ -8,13 +8,12 @@
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
|
||||
|
||||
data class TimelineItemFileContent(
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
val fileSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val formattedFileSize: String,
|
||||
|
||||
@@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAnimatedImage
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT
|
||||
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
@@ -17,7 +16,7 @@ import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
data class TimelineItemImageContent(
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val formattedFileSize: String,
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
|
||||
data class TimelineItemStickerContent(
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val formattedFileSize: String,
|
||||
|
||||
@@ -8,13 +8,12 @@
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class TimelineItemVideoContent(
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
val duration: Duration,
|
||||
val videoSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
|
||||
@@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlin.time.Duration
|
||||
|
||||
@@ -17,7 +16,7 @@ data class TimelineItemVoiceContent(
|
||||
val eventId: EventId?,
|
||||
override val filename: String,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: FormattedBody?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
val duration: Duration,
|
||||
val mediaSource: MediaSource,
|
||||
val mimeType: String,
|
||||
|
||||
@@ -529,7 +529,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onRoomDetailsClick = onRoomDetailsClick,
|
||||
onEventClick = onEventClick,
|
||||
onEventContentClick = onEventClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onPreviewAttachments = onPreviewAttachments,
|
||||
|
||||
@@ -158,7 +158,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onMessageClick = onMessageClick,
|
||||
onContentClick = onMessageClick,
|
||||
onMessageLongClick = onMessageLongClick,
|
||||
onSwipeToReply = onSwipeToReply,
|
||||
onReactionClick = onReactionClick,
|
||||
|
||||
@@ -286,7 +286,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
val expected = TimelineItemVideoContent(
|
||||
filename = "body.mp4",
|
||||
caption = "body.mp4 caption",
|
||||
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
formattedCaption = SpannedString("formatted"),
|
||||
duration = 1.minutes,
|
||||
videoSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
@@ -527,7 +527,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
)
|
||||
val expected = TimelineItemImageContent(
|
||||
filename = "body.jpg",
|
||||
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
|
||||
formattedCaption = SpannedString("formatted"),
|
||||
caption = "body.jpg caption",
|
||||
mediaSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
|
||||
@@ -32,10 +32,8 @@ val localAarProjects = listOf(
|
||||
|
||||
val excludedKoverSubProjects = listOf(
|
||||
":app",
|
||||
":samples",
|
||||
":anvilannotations",
|
||||
":anvilcodegen",
|
||||
":samples:minimal",
|
||||
":tests:testutils",
|
||||
// Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix
|
||||
// SDK api, so it is not really relevant to unit test it: there is no logic to test.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* This will generate the plugin "io.element.android-compose-application" to use by app and samples modules
|
||||
* This will generate the plugin "io.element.android-compose-application" to use by app
|
||||
*/
|
||||
import extension.androidConfig
|
||||
import extension.commonDependencies
|
||||
|
||||
1
samples/minimal/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-compose-application")
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.samples.minimal"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "io.element.android.samples.minimal"
|
||||
targetSdk = Versions.TARGET_SDK
|
||||
versionCode = Versions.VERSION_CODE
|
||||
versionName = Versions.VERSION_NAME
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.preference)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrix.impl)
|
||||
implementation(projects.libraries.permissions.noop)
|
||||
implementation(projects.libraries.sessionStorage.implMemory)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.dateformatter.impl)
|
||||
implementation(projects.libraries.eventformatter.impl)
|
||||
implementation(projects.libraries.fullscreenintent.impl)
|
||||
implementation(projects.libraries.preferences.impl)
|
||||
implementation(projects.libraries.preferences.test)
|
||||
implementation(projects.libraries.indicator.impl)
|
||||
implementation(projects.features.invite.impl)
|
||||
implementation(projects.features.roomlist.impl)
|
||||
implementation(projects.features.leaveroom.impl)
|
||||
implementation(projects.features.login.impl)
|
||||
implementation(projects.features.logout.impl)
|
||||
implementation(projects.features.networkmonitor.impl)
|
||||
implementation(projects.services.toolbox.impl)
|
||||
implementation(projects.libraries.featureflag.impl)
|
||||
implementation(projects.services.analytics.noop)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(projects.libraries.push.test)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2023 New Vector Ltd.
|
||||
~
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
~ Please see LICENSE in the repository root for full details.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.ElementX">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.ElementX">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.samples.minimal
|
||||
|
||||
import io.element.android.libraries.featureflag.api.Feature
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class AlwaysEnabledFeatureFlagService : FeatureFlagService {
|
||||
override fun isFeatureEnabledFlow(feature: Feature): Flow<Boolean> {
|
||||
return flowOf(true)
|
||||
}
|
||||
|
||||
override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.samples.minimal
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordPresenter
|
||||
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordView
|
||||
import io.element.android.features.login.impl.util.defaultAccountProvider
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
|
||||
class LoginScreen(private val authenticationService: MatrixAuthenticationService) {
|
||||
@Composable
|
||||
fun Content(modifier: Modifier = Modifier) {
|
||||
val presenter = remember {
|
||||
LoginPasswordPresenter(
|
||||
authenticationService = authenticationService,
|
||||
AccountProviderDataSource(),
|
||||
DefaultLoginUserStory(),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
authenticationService.setHomeserver(defaultAccountProvider.url)
|
||||
}
|
||||
|
||||
val state = presenter.present()
|
||||
LoginPasswordView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.samples.minimal
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.view.WindowCompat
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.impl.RustClientBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
||||
import io.element.android.libraries.matrix.impl.auth.OidcConfigurationProvider
|
||||
import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||
import io.element.android.libraries.matrix.impl.room.RustTimelineEventTypeFilterFactory
|
||||
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
|
||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
|
||||
import io.element.android.services.analytics.noop.NoopAnalyticsService
|
||||
import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val matrixAuthenticationService: MatrixAuthenticationService by lazy {
|
||||
val baseDirectory = File(applicationContext.filesDir, "sessions")
|
||||
val userAgentProvider = SimpleUserAgentProvider("MinimalSample")
|
||||
val sessionStore = InMemorySessionStore()
|
||||
val userCertificatesProvider = NoOpUserCertificatesProvider()
|
||||
val proxyProvider = NoOpProxyProvider()
|
||||
RustMatrixAuthenticationService(
|
||||
sessionPathsFactory = SessionPathsFactory(baseDirectory, applicationContext.cacheDir),
|
||||
coroutineDispatchers = Singleton.coroutineDispatchers,
|
||||
sessionStore = sessionStore,
|
||||
rustMatrixClientFactory = RustMatrixClientFactory(
|
||||
baseDirectory = baseDirectory,
|
||||
cacheDirectory = applicationContext.cacheDir,
|
||||
appCoroutineScope = Singleton.appScope,
|
||||
coroutineDispatchers = Singleton.coroutineDispatchers,
|
||||
sessionStore = sessionStore,
|
||||
userAgentProvider = userAgentProvider,
|
||||
userCertificatesProvider = userCertificatesProvider,
|
||||
proxyProvider = proxyProvider,
|
||||
clock = DefaultSystemClock(),
|
||||
analyticsService = NoopAnalyticsService(),
|
||||
featureFlagService = AlwaysEnabledFeatureFlagService(),
|
||||
timelineEventTypeFilterFactory = RustTimelineEventTypeFilterFactory(),
|
||||
clientBuilderProvider = RustClientBuilderProvider(),
|
||||
),
|
||||
passphraseGenerator = NullPassphraseGenerator(),
|
||||
oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory),
|
||||
appPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
setContent {
|
||||
ElementTheme {
|
||||
val loggedInState by matrixAuthenticationService.loggedInStateFlow().collectAsState(initial = LoggedInState.NotLoggedIn)
|
||||
Content(isLoggedIn = loggedInState is LoggedInState.LoggedIn, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Content(
|
||||
isLoggedIn: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (!isLoggedIn) {
|
||||
LoginScreen(authenticationService = matrixAuthenticationService).Content(modifier)
|
||||
} else {
|
||||
val matrixClient = runBlocking {
|
||||
val sessionId = matrixAuthenticationService.getLatestSessionId()!!
|
||||
matrixAuthenticationService.restoreSession(sessionId).getOrNull()
|
||||
}
|
||||
RoomListScreen(LocalContext.current, matrixClient!!).Content(modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.samples.minimal
|
||||
|
||||
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
|
||||
|
||||
class NoOpProxyProvider : ProxyProvider {
|
||||
override fun provides(): String? = null
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.samples.minimal
|
||||
|
||||
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
|
||||
|
||||
class NoOpUserCertificatesProvider : UserCertificatesProvider {
|
||||
override fun provides(): List<ByteArray> = emptyList()
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.samples.minimal
|
||||
|
||||
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
|
||||
|
||||
class NullPassphraseGenerator : PassphraseGenerator {
|
||||
override fun generatePassphrase(): String? = null
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.samples.minimal
|
||||
|
||||
import android.net.Uri
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
|
||||
class OnlyFallbackPermalinkParser : PermalinkParser {
|
||||
override fun parse(uriString: String): PermalinkData {
|
||||
return PermalinkData.FallbackLink(Uri.parse(uriString))
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.samples.minimal
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
|
||||
import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
|
||||
import io.element.android.features.leaveroom.impl.LeaveRoomPresenter
|
||||
import io.element.android.features.logout.impl.direct.DirectLogoutPresenter
|
||||
import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor
|
||||
import io.element.android.features.roomlist.impl.RoomListPresenter
|
||||
import io.element.android.features.roomlist.impl.RoomListView
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
||||
import io.element.android.features.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
|
||||
import io.element.android.libraries.androidutils.system.DefaultDateTimeObserver
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.dateformatter.impl.DateFormatters
|
||||
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.eventformatter.impl.DefaultRoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.impl.ProfileChangeContentFormatter
|
||||
import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter
|
||||
import io.element.android.libraries.eventformatter.impl.StateContentFormatter
|
||||
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
|
||||
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.impl.room.join.DefaultJoinRoom
|
||||
import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||
import io.element.android.services.analytics.noop.NoopAnalyticsService
|
||||
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
class RoomListScreen(
|
||||
context: Context,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val coroutineDispatchers: CoroutineDispatchers = Singleton.coroutineDispatchers,
|
||||
) {
|
||||
private val clock = Clock.System
|
||||
private val locale = Locale.getDefault()
|
||||
private val dateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.currentSystemDefault() }
|
||||
private val dateFormatters = DateFormatters(locale, clock) { TimeZone.currentSystemDefault() }
|
||||
private val sessionVerificationService = matrixClient.sessionVerificationService()
|
||||
private val encryptionService = matrixClient.encryptionService()
|
||||
private val stringProvider = AndroidStringProvider(context.resources)
|
||||
private val featureFlagService = AlwaysEnabledFeatureFlagService()
|
||||
private val roomListRoomSummaryFactory = RoomListRoomSummaryFactory(
|
||||
lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(
|
||||
localDateTimeProvider = dateTimeProvider,
|
||||
dateFormatters = dateFormatters
|
||||
),
|
||||
roomLastMessageFormatter = DefaultRoomLastMessageFormatter(
|
||||
sp = stringProvider,
|
||||
roomMembershipContentFormatter = RoomMembershipContentFormatter(
|
||||
matrixClient = matrixClient,
|
||||
sp = stringProvider
|
||||
),
|
||||
profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider),
|
||||
stateContentFormatter = StateContentFormatter(stringProvider),
|
||||
permalinkParser = OnlyFallbackPermalinkParser(),
|
||||
),
|
||||
)
|
||||
private val presenter = RoomListPresenter(
|
||||
client = matrixClient,
|
||||
networkMonitor = DefaultNetworkMonitor(context, Singleton.appScope),
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
leaveRoomPresenter = LeaveRoomPresenter(matrixClient, RoomMembershipObserver(), coroutineDispatchers),
|
||||
roomListDataSource = RoomListDataSource(
|
||||
roomListService = matrixClient.roomListService,
|
||||
roomListRoomSummaryFactory = roomListRoomSummaryFactory,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
notificationSettingsService = matrixClient.notificationSettingsService(),
|
||||
appScope = Singleton.appScope,
|
||||
dateTimeObserver = DefaultDateTimeObserver(context),
|
||||
),
|
||||
indicatorService = DefaultIndicatorService(
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
encryptionService = encryptionService,
|
||||
),
|
||||
featureFlagService = featureFlagService,
|
||||
searchPresenter = RoomListSearchPresenter(
|
||||
RoomListSearchDataSource(
|
||||
roomListService = matrixClient.roomListService,
|
||||
roomSummaryFactory = roomListRoomSummaryFactory,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
),
|
||||
featureFlagService = featureFlagService,
|
||||
),
|
||||
sessionPreferencesStore = DefaultSessionPreferencesStore(
|
||||
context = context,
|
||||
sessionId = matrixClient.sessionId,
|
||||
sessionCoroutineScope = Singleton.appScope
|
||||
),
|
||||
filtersPresenter = RoomListFiltersPresenter(
|
||||
roomListService = matrixClient.roomListService,
|
||||
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
|
||||
),
|
||||
acceptDeclineInvitePresenter = AcceptDeclineInvitePresenter(
|
||||
client = matrixClient,
|
||||
joinRoom = DefaultJoinRoom(matrixClient, NoopAnalyticsService()),
|
||||
notificationCleaner = FakeNotificationCleaner(),
|
||||
),
|
||||
analyticsService = NoopAnalyticsService(),
|
||||
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
|
||||
notificationCleaner = FakeNotificationCleaner(),
|
||||
logoutPresenter = DirectLogoutPresenter(matrixClient, encryptionService),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun Content(modifier: Modifier = Modifier) {
|
||||
fun onRoomClick(roomId: RoomId) {
|
||||
Singleton.appScope.launch {
|
||||
withContext(coroutineDispatchers.io) {
|
||||
matrixClient.getRoom(roomId)!!.use { room ->
|
||||
room.liveTimeline.paginate(Timeline.PaginationDirection.BACKWARDS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val state = presenter.present()
|
||||
RoomListView(
|
||||
state = state,
|
||||
onRoomClick = ::onRoomClick,
|
||||
onSettingsClick = {},
|
||||
onSetUpRecoveryClick = {},
|
||||
onConfirmRecoveryKeyClick = {},
|
||||
onCreateRoomClick = {},
|
||||
onRoomSettingsClick = {},
|
||||
onMenuActionClick = {},
|
||||
onRoomDirectorySearchClick = {},
|
||||
modifier = modifier,
|
||||
acceptDeclineInviteView = {
|
||||
AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onAcceptInvite = {}, onDeclineInvite = {})
|
||||
},
|
||||
onMigrateToNativeSlidingSyncClick = {},
|
||||
)
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
Timber.w("Start sync!")
|
||||
runBlocking {
|
||||
matrixClient.syncService().startSync()
|
||||
}
|
||||
onDispose {
|
||||
Timber.w("Stop sync!")
|
||||
runBlocking {
|
||||
matrixClient.syncService().stopSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.samples.minimal
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
|
||||
import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
|
||||
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
|
||||
import io.element.android.libraries.matrix.impl.tracing.RustTracingService
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.plus
|
||||
|
||||
object Singleton {
|
||||
val buildMeta = BuildMeta(
|
||||
isDebuggable = true,
|
||||
buildType = BuildType.DEBUG,
|
||||
applicationName = "EAX-Minimal",
|
||||
productionApplicationName = "EAX-Minimal",
|
||||
desktopApplicationName = "EAX-Minimal-Desktop",
|
||||
applicationId = "io.element.android.samples.minimal",
|
||||
isEnterpriseBuild = false,
|
||||
lowPrivacyLoggingEnabled = false,
|
||||
versionName = "0.1.0",
|
||||
versionCode = 1,
|
||||
gitRevision = "",
|
||||
gitBranchName = "",
|
||||
flavorDescription = "NA",
|
||||
flavorShortDescription = "NA",
|
||||
)
|
||||
|
||||
init {
|
||||
val tracingConfiguration = TracingConfiguration(
|
||||
filterConfiguration = TracingFilterConfigurations.debug,
|
||||
writesToLogcat = true,
|
||||
writesToFilesConfiguration = WriteToFilesConfiguration.Disabled
|
||||
)
|
||||
RustTracingService(buildMeta).setupTracing(tracingConfiguration)
|
||||
}
|
||||
|
||||
val appScope = MainScope() + CoroutineName("Minimal Scope")
|
||||
val coroutineDispatchers = CoroutineDispatchers(
|
||||
io = Dispatchers.IO,
|
||||
computation = Dispatchers.Default,
|
||||
main = Dispatchers.Main,
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2023 New Vector Ltd.
|
||||
~
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
~ Please see LICENSE in the repository root for full details.
|
||||
-->
|
||||
<resources>
|
||||
<style name="Theme.ElementX" parent="android:Theme.Material.NoActionBar" />
|
||||
</resources>
|
||||
@@ -1,9 +0,0 @@
|
||||
<!--
|
||||
~ Copyright 2023 New Vector Ltd.
|
||||
~
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
~ Please see LICENSE in the repository root for full details.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">EAX-Sample</string>
|
||||
</resources>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2023 New Vector Ltd.
|
||||
~
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
~ Please see LICENSE in the repository root for full details.
|
||||
-->
|
||||
<resources>
|
||||
<style name="Theme.ElementX" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
@@ -70,8 +70,6 @@ include(":tests:testutils")
|
||||
include(":anvilannotations")
|
||||
include(":anvilcodegen")
|
||||
|
||||
include(":samples:minimal")
|
||||
|
||||
fun includeProjects(directory: File, path: String, maxDepth: Int = 1) {
|
||||
directory.listFiles().orEmpty().also { it.sort() }.forEach { file ->
|
||||
if (file.isDirectory) {
|
||||
|
||||
@@ -18,5 +18,4 @@ set -e
|
||||
./gradlew runQualityChecks
|
||||
|
||||
# Build, test and check the project, with warning as errors
|
||||
# It also check that the minimal app is compiling.
|
||||
./gradlew check -PallWarningsAsErrors=true
|
||||
|
||||