Merge develop into feature/fga/update_rust_sdk

This commit is contained in:
ganfra 2023-02-24 11:14:36 +01:00
commit 22fd4ac7f0
410 changed files with 2937 additions and 607 deletions

View file

@ -67,7 +67,8 @@ class MessagesPresenter @Inject constructor(
LaunchedEffect(syncUpdateFlow) {
roomAvatar.value =
AvatarData(
name = room.bestName,
id = room.roomId.value,
name = room.name,
url = room.avatarUrl,
size = AvatarSize.SMALL
)

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.actionlist.anActionListState
import io.element.android.features.messages.textcomposer.aMessageComposerState
import io.element.android.features.messages.timeline.aTimelineItemContent
import io.element.android.features.messages.timeline.aTimelineItemList
import io.element.android.features.messages.timeline.aTimelineState
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.textcomposer.MessageComposerMode
open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
override val values: Sequence<MessagesState>
get() = sequenceOf(
aMessagesState(),
)
}
fun aMessagesState() = MessagesState(
roomId = RoomId("!id"),
roomName = "Room name",
roomAvatar = AvatarData("!id", "Room name"),
composerState = aMessageComposerState().copy(
text = StableCharSequence("Hello"),
isFullScreen = false,
mode = MessageComposerMode.Normal("Hello"),
),
timelineState = aTimelineState().copy(
timelineItems = aTimelineItemList(aTimelineItemContent()),
),
actionListState = anActionListState(),
eventSink = {}
)

View file

@ -49,6 +49,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.messages.actionlist.ActionListEvents
@ -59,6 +61,8 @@ import io.element.android.features.messages.timeline.TimelineView
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -72,7 +76,7 @@ import timber.log.Timber
fun MessagesView(
state: MessagesState,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit,
onBackPressed: () -> Unit = {},
) {
LogCompositions(tag = "MessagesScreen", msg = "Root")
val itemActionsBottomSheetState = rememberModalBottomSheetState(
@ -197,6 +201,20 @@ fun MessagesViewTopBar(
)
}
}
)
}
@Preview
@Composable
internal fun MessagesViewLightPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
internal fun MessagesViewDarkPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: MessagesState) {
MessagesView(state)
}

View file

@ -26,7 +26,6 @@ data class ActionListState(
val target: Target,
val eventSink: (ActionListEvents) -> Unit,
) {
sealed interface Target {
object None : Target
data class Loading(val event: TimelineItem.Event) : Target

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.actionlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.timeline.aTimelineItemEvent
import kotlinx.collections.immutable.persistentListOf
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
override val values: Sequence<ActionListState>
get() = sequenceOf(
anActionListState(),
anActionListState().copy(target = ActionListState.Target.Loading(aTimelineItemEvent())),
anActionListState().copy(
target = ActionListState.Target.Success(
event = aTimelineItemEvent(),
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Copy,
TimelineItemAction.Edit,
TimelineItemAction.Redact,
)
)
)
)
}
fun anActionListState() = ActionListState(
target = ActionListState.Target.None,
eventSink = {}
)

View file

@ -28,7 +28,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
import androidx.compose.material.LocalContentColor
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
@ -38,11 +37,14 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.messages.actionlist.model.TimelineItemAction
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.libraries.designsystem.components.VectorIcon
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
@ -115,13 +117,14 @@ private fun SheetContent(
text = {
Text(
text = action.title,
color = if (action.destructive) MaterialTheme.colorScheme.error else Color.Unspecified,
color = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
)
},
icon = {
VectorIcon(
Icon(
resourceId = action.icon,
tint = if (action.destructive) MaterialTheme.colorScheme.error else LocalContentColor.current,
contentDescription = "",
tint = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
)
}
)
@ -130,3 +133,18 @@ private fun SheetContent(
}
}
}
@Preview
@Composable
fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: ActionListState) {
SheetContent(state)
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.textcomposer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.textcomposer.MessageComposerMode
open class MessageComposerStateProvider : PreviewParameterProvider<MessageComposerState> {
override val values: Sequence<MessageComposerState>
get() = sequenceOf(
aMessageComposerState(),
)
}
fun aMessageComposerState() = MessageComposerState(
text = StableCharSequence(""),
isFullScreen = false,
mode = MessageComposerMode.Normal(content = ""),
eventSink = {}
)

View file

@ -18,6 +18,10 @@ package io.element.android.features.messages.textcomposer
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.textcomposer.TextComposer
@ -55,3 +59,18 @@ fun MessageComposerView(
modifier = modifier
)
}
@Preview
@Composable
internal fun MessageComposerViewLightPreview(@PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
internal fun MessageComposerViewDarkPreview(@PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: MessageComposerState) {
MessageComposerView(state)
}

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.timeline.model.event.TimelineItemTextContent
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.EventId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
fun aTimelineState() = TimelineState(
timelineItems = persistentListOf(),
highlightedEventId = null,
eventSink = {}
)
internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList<TimelineItem> {
return persistentListOf(
// 3 items (First Middle Last) with isMine = false
aTimelineItemEvent(
isMine = false,
content = content,
groupPosition = TimelineItemGroupPosition.Last
),
aTimelineItemEvent(
isMine = false,
content = content,
groupPosition = TimelineItemGroupPosition.Middle
),
aTimelineItemEvent(
isMine = false,
content = content,
groupPosition = TimelineItemGroupPosition.First
),
// 3 items (First Middle Last) with isMine = true
aTimelineItemEvent(
isMine = true,
content = content,
groupPosition = TimelineItemGroupPosition.Last
),
aTimelineItemEvent(
isMine = true,
content = content,
groupPosition = TimelineItemGroupPosition.Middle
),
aTimelineItemEvent(
isMine = true,
content = content,
groupPosition = TimelineItemGroupPosition.First
),
)
}
internal fun aTimelineItemEvent(
isMine: Boolean = false,
content: TimelineItemEventContent = aTimelineItemContent(),
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First
): TimelineItem.Event {
val randomId = Math.random().toString()
return TimelineItem.Event(
id = randomId,
eventId = EventId(randomId),
senderId = "@senderId",
senderAvatar = AvatarData("@senderId", "sender"),
content = content,
reactionsState = TimelineItemReactions(
persistentListOf(
AggregatedReaction("👍", "1")
)
),
isMine = isMine,
senderDisplayName = "sender",
groupPosition = groupPosition,
)
}
internal fun aTimelineItemContent(): TimelineItemEventContent {
return TimelineItemTextContent(
body = "Text",
htmlDocument = null
)
}

View file

@ -54,6 +54,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.element.android.features.messages.timeline.model.bubble.BubbleState
import io.element.android.features.messages.timeline.components.MessageEventBubble
import io.element.android.features.messages.timeline.components.TimelineItemEncryptedView
import io.element.android.features.messages.timeline.components.TimelineItemImageView
@ -63,11 +64,7 @@ import io.element.android.features.messages.timeline.components.TimelineItemText
import io.element.android.features.messages.timeline.components.TimelineItemUnknownView
import io.element.android.features.messages.timeline.components.virtual.TimelineItemDaySeparatorView
import io.element.android.features.messages.timeline.components.virtual.TimelineLoadingMoreIndicator
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.features.messages.timeline.model.event.MessagesTimelineItemContentProvider
import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent
@ -76,6 +73,7 @@ import io.element.android.features.messages.timeline.model.event.TimelineItemTex
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.features.messages.timeline.model.virtual.TimelineItemLoadingModel
import io.element.android.features.messages.timeline.model.event.TimelineItemEventContentProvider
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -84,7 +82,6 @@ import io.element.android.libraries.designsystem.theme.components.FloatingAction
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@ -221,10 +218,12 @@ fun TimelineItemEventRow(
)
}
MessageEventBubble(
groupPosition = event.groupPosition,
isMine = event.isMine,
state = BubbleState(
groupPosition = event.groupPosition,
isMine = event.isMine,
isHighlighted = isHighlighted,
),
interactionSource = interactionSource,
isHighlighted = isHighlighted,
onClick = onClick,
onLongClick = onLongClick,
modifier = Modifier
@ -359,78 +358,22 @@ internal fun BoxScope.TimelineScrollHelper(
@Preview
@Composable
fun LoginRootScreenLightPreview(
@PreviewParameter(MessagesTimelineItemContentProvider::class) content: TimelineItemEventContent
fun TimelineViewLightPreview(
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
) = ElementPreviewLight { ContentToPreview(content) }
@Preview
@Composable
fun LoginRootScreenDarkPreview(
@PreviewParameter(MessagesTimelineItemContentProvider::class) content: TimelineItemEventContent
fun TimelineViewDarkPreview(
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
) = ElementPreviewDark { ContentToPreview(content) }
@Composable
private fun ContentToPreview(content: TimelineItemEventContent) {
val timelineItems = persistentListOf(
// 3 items (First Middle Last) with isMine = false
createMessageEvent(
isMine = false,
content = content,
groupPosition = MessagesItemGroupPosition.Last
),
createMessageEvent(
isMine = false,
content = content,
groupPosition = MessagesItemGroupPosition.Middle
),
createMessageEvent(
isMine = false,
content = content,
groupPosition = MessagesItemGroupPosition.First
),
// 3 items (First Middle Last) with isMine = true
createMessageEvent(
isMine = true,
content = content,
groupPosition = MessagesItemGroupPosition.Last
),
createMessageEvent(
isMine = true,
content = content,
groupPosition = MessagesItemGroupPosition.Middle
),
createMessageEvent(
isMine = true,
content = content,
groupPosition = MessagesItemGroupPosition.First
),
)
val timelineItems = aTimelineItemList(content)
TimelineView(
state = TimelineState(
state = aTimelineState().copy(
timelineItems = timelineItems,
highlightedEventId = null,
eventSink = {}
)
)
}
private fun createMessageEvent(
isMine: Boolean,
content: TimelineItemEventContent,
groupPosition: MessagesItemGroupPosition
): TimelineItem {
return TimelineItem.Event(
id = Math.random().toString(),
senderId = "senderId",
senderAvatar = AvatarData("sender"),
content = content,
reactionsState = TimelineItemReactions(
persistentListOf(
AggregatedReaction("👍", "1")
)
),
isMine = isMine,
senderDisplayName = "sender",
groupPosition = groupPosition,
)
}

View file

@ -19,16 +19,27 @@ package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.timeline.model.bubble.BubbleState
import io.element.android.features.messages.timeline.model.bubble.BubbleStateProvider
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Surface
@ -37,33 +48,31 @@ private val BUBBLE_RADIUS = 16.dp
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MessageEventBubble(
groupPosition: MessagesItemGroupPosition,
isMine: Boolean,
state: BubbleState,
interactionSource: MutableInteractionSource,
isHighlighted: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
content: @Composable () -> Unit = {},
) {
fun bubbleShape(): Shape {
return when (groupPosition) {
MessagesItemGroupPosition.First -> if (isMine) {
return when (state.groupPosition) {
TimelineItemGroupPosition.First -> if (state.isMine) {
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS)
} else {
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp)
}
MessagesItemGroupPosition.Middle -> if (isMine) {
TimelineItemGroupPosition.Middle -> if (state.isMine) {
RoundedCornerShape(BUBBLE_RADIUS, 0.dp, 0.dp, BUBBLE_RADIUS)
} else {
RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp)
}
MessagesItemGroupPosition.Last -> if (isMine) {
TimelineItemGroupPosition.Last -> if (state.isMine) {
RoundedCornerShape(BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS)
} else {
RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS)
}
MessagesItemGroupPosition.None ->
TimelineItemGroupPosition.None ->
RoundedCornerShape(
BUBBLE_RADIUS,
BUBBLE_RADIUS,
@ -74,17 +83,17 @@ fun MessageEventBubble(
}
fun Modifier.offsetForItem(): Modifier {
return if (isMine) {
return if (state.isMine) {
offset(y = -(12.dp))
} else {
offset(x = 20.dp, y = -(12.dp))
}
}
val backgroundBubbleColor = if (isHighlighted) {
val backgroundBubbleColor = if (state.isHighlighted) {
ElementTheme.colors.messageHighlightedBackground
} else {
if (isMine) {
if (state.isMine) {
ElementTheme.colors.messageFromMeBackground
} else {
ElementTheme.colors.messageFromOtherBackground
@ -107,3 +116,31 @@ fun MessageEventBubble(
content = content
)
}
@Preview
@Composable
internal fun MessageEventBubbleLightPreview(@PreviewParameter(BubbleStateProvider::class) state: BubbleState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
internal fun MessageEventBubbleDarkPreview(@PreviewParameter(BubbleStateProvider::class) state: BubbleState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: BubbleState) {
// Due to y offset, surround with a Box
Box(
modifier = Modifier
.size(width = 240.dp, height = 64.dp)
.padding(8.dp),
contentAlignment = Alignment.CenterStart,
) {
MessageEventBubble(
state = state,
interactionSource = MutableInteractionSource(),
) {
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
}
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.AggregatedReactionProvider
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.surfaceVariant,
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background),
shape = RoundedCornerShape(corner = CornerSize(12.dp)),
) {
Row(
modifier = Modifier.padding(vertical = 5.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// TODO `reaction.isHighlighted` is not used.
Text(text = reaction.key, fontSize = 12.sp)
Spacer(modifier = Modifier.width(4.dp))
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
}
}
}
@Preview
@Composable
internal fun MessagesReactionButtonLightPreview(@PreviewParameter(AggregatedReactionProvider::class) reaction: AggregatedReaction) =
ElementPreviewLight { ContentToPreview(reaction) }
@Preview
@Composable
internal fun MessagesReactionButtonDarkPreview(@PreviewParameter(AggregatedReactionProvider::class) reaction: AggregatedReaction) =
ElementPreviewDark { ContentToPreview(reaction) }
@Composable
private fun ContentToPreview(reaction: AggregatedReaction) {
MessagesReactionButton(reaction)
}

View file

@ -20,7 +20,11 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.features.messages.timeline.model.event.TimelineItemEncryptedContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import org.matrix.rustcomponents.sdk.EncryptedMessage
@Composable
fun TimelineItemEncryptedView(
@ -34,3 +38,22 @@ fun TimelineItemEncryptedView(
modifier = modifier
)
}
@Preview
@Composable
internal fun TimelineItemEncryptedViewLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun TimelineItemEncryptedViewDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
TimelineItemEncryptedView(
content = TimelineItemEncryptedContent(
encryptedMessage = EncryptedMessage.Unknown,
)
)
}

View file

@ -28,9 +28,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.timeline.model.event.TimelineItemImageContentProvider
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
@Composable
fun TimelineItemImageView(
@ -48,7 +54,7 @@ fun TimelineItemImageView(
.aspectRatio(content.aspectRatio),
contentAlignment = Alignment.Center,
) {
var isLoading = rememberSaveable(content.imageMeta) { mutableStateOf(true) }
val isLoading = rememberSaveable(content.imageMeta) { mutableStateOf(true) }
val context = LocalContext.current
val model = ImageRequest.Builder(context)
.data(content.imageMeta)
@ -57,9 +63,24 @@ fun TimelineItemImageView(
AsyncImage(
model = model,
contentDescription = null,
placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant),
placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)),
contentScale = ContentScale.Crop,
onSuccess = { isLoading.value = false },
)
}
}
@Preview
@Composable
internal fun TimelineItemImageViewLightPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) =
ElementPreviewLight { ContentToPreview(content) }
@Preview
@Composable
internal fun TimelineItemImageViewDarkPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) =
ElementPreviewDark { ContentToPreview(content) }
@Composable
private fun ContentToPreview(content: TimelineItemImageContent) {
TimelineItemImageView(content)
}

View file

@ -65,11 +65,11 @@ fun TimelineItemInformativeView(
@Preview
@Composable
fun MatrixUserRowLightPreview() = ElementPreviewLight { ContentToPreview() }
internal fun TimelineItemInformativeViewLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
fun MatrixUserRowDarkPreview() = ElementPreviewDark { ContentToPreview() }
internal fun TimelineItemInformativeViewDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {

View file

@ -16,24 +16,15 @@
package io.element.android.features.messages.timeline.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.features.messages.timeline.model.aTimelineItemReactions
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable
fun TimelineItemReactionsView(
@ -52,21 +43,19 @@ fun TimelineItemReactionsView(
}
}
@Preview
@Composable
fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.surfaceVariant,
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background),
shape = RoundedCornerShape(corner = CornerSize(12.dp)),
) {
Row(
modifier = Modifier.padding(vertical = 5.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = reaction.key, fontSize = 12.sp)
Spacer(modifier = Modifier.width(4.dp))
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
}
}
internal fun TimelineItemReactionsViewLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun TimelineItemReactionsViewDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
TimelineItemReactionsView(
reactionsState = aTimelineItemReactions()
)
}

View file

@ -20,7 +20,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable
fun TimelineItemRedactedView(
@ -34,3 +37,18 @@ fun TimelineItemRedactedView(
modifier = modifier
)
}
@Preview
@Composable
internal fun TimelineItemRedactedViewLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun TimelineItemRedactedViewDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
TimelineItemRedactedView(TimelineItemRedactedContent)
}

View file

@ -27,11 +27,16 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.core.text.util.LinkifyCompat
import io.element.android.features.messages.timeline.components.html.HtmlDocument
import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.timeline.model.event.TimelineItemTextBasedContentProvider
import io.element.android.libraries.designsystem.LinkColor
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable
fun TimelineItemTextView(
@ -91,3 +96,19 @@ private fun String.linkify(
)
}
}
@Preview
@Composable
internal fun TimelineItemTextViewLightPreview(@PreviewParameter(TimelineItemTextBasedContentProvider::class) content: TimelineItemTextBasedContent) =
ElementPreviewLight { ContentToPreview(content) }
@Preview
@Composable
internal fun TimelineItemTextViewDarkPreview(@PreviewParameter(TimelineItemTextBasedContentProvider::class) content: TimelineItemTextBasedContent) =
ElementPreviewDark { ContentToPreview(content) }
@Composable
fun ContentToPreview(content: TimelineItemTextBasedContent) {
TimelineItemTextView(content, MutableInteractionSource())
}

View file

@ -20,7 +20,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.features.messages.timeline.model.event.TimelineItemUnknownContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable
fun TimelineItemUnknownView(
@ -34,3 +37,18 @@ fun TimelineItemUnknownView(
modifier = modifier
)
}
@Preview
@Composable
internal fun TimelineItemUnknownViewLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun TimelineItemUnknownViewDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
TimelineItemUnknownView(TimelineItemUnknownContent)
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.components.html
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
open class DocumentProvider : PreviewParameterProvider<Document> {
override val values: Sequence<Document>
get() = sequenceOf(
"text",
"<strong>Strong</strong>",
"<b>Bold</b>",
"<i>Italic</i>",
// FIXME This does not work
"<b><i>Bold then italic</i></b>",
// FIXME This does not work
"<i><b>Italic then bold</b></i>",
"<em>em</em>",
"<unknown>unknown</unknown>",
// FIXME `br` is not rendered correctly in the Preview.
"Line 1<br/>Line 2",
"<code>code</code>",
"<del>del</del>",
"<h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3><h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6><h7>Heading 7</h7>",
"<a href=\"https://matrix.org\">link</a>",
"<p>paragraph</p>",
"<p>paragraph 1</p><p>paragraph 2</p>",
"<ol><li>ol item 1</li><li>ol item 2</li></ol>",
"<ol><li><i>ol item 1 italic</i></li><li><b>ol item 2 bold</b></li></ol>",
"<ul><li>ul item 1</li><li>ul item 2</li></ul>",
"<blockquote>blockquote</blockquote>",
// TODO Find a way to make is work with `pre`. For now there is an error with
// jsoup: java.lang.NoSuchMethodError: 'org.jsoup.nodes.Element org.jsoup.nodes.Element.firstElementChild()'
// "<pre>pre</pre>",
"<mx-reply><blockquote><a href=\\\"https://matrix.to/#/!roomId/\$eventId?via=matrix.org\\\">In reply to</a> " +
"<a href=\\\"https://matrix.to/#/@alice:matrix.org\\\">@alice:matrix.org</a><br>original message</blockquote></mx-reply>reply",
).map { Jsoup.parse(it) }
}

View file

@ -42,11 +42,15 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import io.element.android.libraries.designsystem.LinkColor
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.permalink.PermalinkData
@ -99,7 +103,10 @@ private fun HtmlBody(
when (val node = nodes.next()) {
is TextNode -> {
if (!node.isBlank) {
Text(text = node.text())
Text(
text = node.text(),
color = MaterialTheme.colorScheme.primary,
)
}
}
is Element -> {
@ -139,7 +146,7 @@ private fun HtmlBody(
}
private fun Element.isInline(): Boolean {
return when (normalName()) {
return when (tagName().lowercase()) {
"del" -> true
"mx-reply" -> false
else -> !isBlock
@ -156,7 +163,7 @@ private fun HtmlBlock(
) {
val blockModifier = modifier
.padding(top = 4.dp)
when (element.normalName()) {
when (element.tagName().lowercase()) {
"p" -> HtmlParagraph(
paragraph = element,
modifier = blockModifier,
@ -230,7 +237,7 @@ private fun HtmlPreformatted(
pre: Element,
modifier: Modifier = Modifier
) {
val isCode = pre.firstElementChild()?.normalName() == "code"
val isCode = pre.firstElementChild()?.tagName()?.lowercase() == "code"
val backgroundColor =
if (isCode) MaterialTheme.colorScheme.codeBackground() else Color.Unspecified
Box(
@ -241,6 +248,7 @@ private fun HtmlPreformatted(
Text(
text = pre.wholeText(),
style = TextStyle(fontFamily = FontFamily.Monospace),
color = MaterialTheme.colorScheme.primary,
)
}
}
@ -305,7 +313,7 @@ private fun HtmlHeading(
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
val style = when (heading.normalName()) {
val style = when (heading.tagName().lowercase()) {
"h1" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 30.sp)
"h2" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 26.sp)
"h3" -> MaterialTheme.typography.headlineMedium.copy(fontSize = 22.sp)
@ -361,7 +369,7 @@ private fun HtmlMxReply(
}
}
is Element -> {
when (blockquoteNode.normalName()) {
when (blockquoteNode.tagName().lowercase()) {
"br" -> {
append('\n')
}
@ -483,7 +491,7 @@ private fun AnnotatedString.Builder.appendInlineChildrenElements(
}
private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors: ColorScheme) {
when (element.normalName()) {
when (element.tagName().lowercase()) {
"br" -> {
append('\n')
}
@ -502,6 +510,7 @@ private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors
appendInlineChildrenElements(element.childNodes(), colors)
}
}
"i",
"em" -> {
withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
appendInlineChildrenElements(element.childNodes(), colors)
@ -567,3 +576,18 @@ private fun HtmlText(
onLongClick = onLongClick
)
}
@Preview
@Composable
internal fun HtmlDocumentLightPreview(@PreviewParameter(DocumentProvider::class) document: Document) =
ElementPreviewLight { ContentToPreview(document) }
@Preview
@Composable
internal fun HtmlDocumentDarkPreview(@PreviewParameter(DocumentProvider::class) document: Document) =
ElementPreviewDark { ContentToPreview(document) }
@Composable
private fun ContentToPreview(document: Document) {
HtmlDocument(document, MutableInteractionSource())
}

View file

@ -17,8 +17,8 @@
package io.element.android.features.messages.timeline.factories.event
import io.element.android.features.messages.timeline.model.AggregatedReaction
import io.element.android.features.messages.timeline.model.MessagesItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItem
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.timeline.model.TimelineItemReactions
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -58,6 +58,7 @@ class TimelineItemEventFactory @Inject constructor(
}
val senderAvatarData = AvatarData(
id = currentSender,
name = senderDisplayName ?: currentSender,
url = senderAvatarUrl,
size = AvatarSize.SMALL
@ -86,7 +87,7 @@ class TimelineItemEventFactory @Inject constructor(
currentTimelineItem: MatrixTimelineItem.Event,
timelineItems: List<MatrixTimelineItem>,
index: Int
): MessagesItemGroupPosition {
): TimelineItemGroupPosition {
val prevTimelineItem =
timelineItems.getOrNull(index - 1) as? MatrixTimelineItem.Event
val nextTimelineItem =
@ -96,10 +97,10 @@ class TimelineItemEventFactory @Inject constructor(
val nextSender = nextTimelineItem?.event?.sender()
return when {
previousSender != currentSender && nextSender == currentSender -> MessagesItemGroupPosition.First
previousSender == currentSender && nextSender == currentSender -> MessagesItemGroupPosition.Middle
previousSender == currentSender && nextSender != currentSender -> MessagesItemGroupPosition.Last
else -> MessagesItemGroupPosition.None
previousSender != currentSender && nextSender == currentSender -> TimelineItemGroupPosition.First
previousSender == currentSender && nextSender == currentSender -> TimelineItemGroupPosition.Middle
previousSender == currentSender && nextSender != currentSender -> TimelineItemGroupPosition.Last
else -> TimelineItemGroupPosition.None
}
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model
data class AggregatedReaction(
val key: String,
val count: String,
val isHighlighted: Boolean = false
)

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class AggregatedReactionProvider : PreviewParameterProvider<AggregatedReaction> {
override val values: Sequence<AggregatedReaction>
get() = sequenceOf(
anAggregatedReaction(),
anAggregatedReaction().copy(count = "88"),
anAggregatedReaction().copy(isHighlighted = true),
anAggregatedReaction().copy(count = "88", isHighlighted = true),
)
}
fun anAggregatedReaction() = AggregatedReaction(
key = "👍",
count = "1", // TODO Why is it a String?
isHighlighted = false,
)

View file

@ -46,7 +46,7 @@ sealed interface TimelineItem {
val content: TimelineItemEventContent,
val sentTime: String = "",
val isMine: Boolean = false,
val groupPosition: MessagesItemGroupPosition = MessagesItemGroupPosition.None,
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
val reactionsState: TimelineItemReactions
) : TimelineItem {

View file

@ -17,14 +17,13 @@
package io.element.android.features.messages.timeline.model
import androidx.compose.runtime.Immutable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@Immutable
sealed interface MessagesItemGroupPosition {
object First : MessagesItemGroupPosition
object Middle : MessagesItemGroupPosition
object Last : MessagesItemGroupPosition
object None : MessagesItemGroupPosition
sealed interface TimelineItemGroupPosition {
object First : TimelineItemGroupPosition
object Middle : TimelineItemGroupPosition
object Last : TimelineItemGroupPosition
object None : TimelineItemGroupPosition
fun isNew(): Boolean = when (this) {
First, None -> true
@ -32,11 +31,3 @@ sealed interface MessagesItemGroupPosition {
}
}
internal class TimelineItemGroupPositionProvider : PreviewParameterProvider<MessagesItemGroupPosition> {
override val values = sequenceOf(
MessagesItemGroupPosition.First,
MessagesItemGroupPosition.Middle,
MessagesItemGroupPosition.Last,
MessagesItemGroupPosition.None,
)
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
internal class TimelineItemGroupPositionProvider : PreviewParameterProvider<TimelineItemGroupPosition> {
override val values = sequenceOf(
TimelineItemGroupPosition.First,
TimelineItemGroupPosition.Middle,
TimelineItemGroupPosition.Last,
TimelineItemGroupPosition.None,
)
}

View file

@ -21,9 +21,3 @@ import kotlinx.collections.immutable.ImmutableList
data class TimelineItemReactions(
val reactions: ImmutableList<AggregatedReaction>
)
data class AggregatedReaction(
val key: String,
val count: String,
val isHighlighted: Boolean = false
)

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model
import kotlinx.collections.immutable.toPersistentList
fun aTimelineItemReactions() = TimelineItemReactions(
// Use values from AggregatedReactionProvider
reactions = AggregatedReactionProvider().values.toPersistentList()
)

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model.bubble
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
data class BubbleState(
val groupPosition: TimelineItemGroupPosition,
val isMine: Boolean,
val isHighlighted: Boolean,
)

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model.bubble
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.timeline.model.TimelineItemGroupPosition
open class BubbleStateProvider : PreviewParameterProvider<BubbleState> {
override val values: Sequence<BubbleState>
get() = sequenceOf(
TimelineItemGroupPosition.First,
TimelineItemGroupPosition.Middle,
TimelineItemGroupPosition.Last,
).map { groupPosition ->
sequenceOf(false, true).map { isMine ->
sequenceOf(false, true).map { isHighlighted ->
BubbleState(groupPosition, isMine = isMine, isHighlighted = isHighlighted)
}
}
.flatten()
}
.flatten()
}
fun aBubbleState() = BubbleState(
groupPosition = TimelineItemGroupPosition.First,
isMine = false,
isHighlighted = false,
)

View file

@ -17,31 +17,6 @@
package io.element.android.features.messages.timeline.model.event
import androidx.compose.runtime.Immutable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.matrix.rustcomponents.sdk.EncryptedMessage
@Immutable
sealed interface TimelineItemEventContent
class MessagesTimelineItemContentProvider : PreviewParameterProvider<TimelineItemEventContent> {
override val values = sequenceOf(
TimelineItemEmoteContent(
body = "Emote",
htmlDocument = null
),
TimelineItemEncryptedContent(
encryptedMessage = EncryptedMessage.Unknown
),
// TODO MessagesTimelineItemImageContent(),
TimelineItemNoticeContent(
body = "Notice",
htmlDocument = null
),
TimelineItemRedactedContent,
TimelineItemTextContent(
body = "Text",
htmlDocument = null
),
TimelineItemUnknownContent,
)
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model.event
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.jsoup.Jsoup
import org.matrix.rustcomponents.sdk.EncryptedMessage
class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEventContent> {
override val values = sequenceOf(
aTimelineItemEmoteContent(),
aTimelineItemEncryptedContent(),
// TODO MessagesTimelineItemImageContent(),
aTimelineItemNoticeContent(),
aTimelineItemRedactedContent(),
aTimelineItemTextContent(),
aTimelineItemUnknownContent(),
)
}
class TimelineItemTextBasedContentProvider : PreviewParameterProvider<TimelineItemTextBasedContent> {
override val values = sequenceOf(
aTimelineItemEmoteContent(),
aTimelineItemEmoteContent().copy(htmlDocument = Jsoup.parse("Emote")),
aTimelineItemNoticeContent(),
aTimelineItemNoticeContent().copy(htmlDocument = Jsoup.parse("Notice")),
aTimelineItemTextContent(),
aTimelineItemTextContent().copy(htmlDocument = Jsoup.parse("Text")),
)
}
fun aTimelineItemEmoteContent() = TimelineItemEmoteContent(
body = "Emote",
htmlDocument = null
)
fun aTimelineItemEncryptedContent() = TimelineItemEncryptedContent(
encryptedMessage = EncryptedMessage.Unknown
)
fun aTimelineItemNoticeContent() = TimelineItemNoticeContent(
body = "Notice",
htmlDocument = null
)
fun aTimelineItemRedactedContent() = TimelineItemRedactedContent
fun aTimelineItemTextContent() = TimelineItemTextContent(
body = "Text",
htmlDocument = null
)
fun aTimelineItemUnknownContent() = TimelineItemUnknownContent

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.timeline.model.event
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.media.MediaResolver
open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineItemImageContent> {
override val values: Sequence<TimelineItemImageContent>
get() = sequenceOf(
aTimelineItemImageContent(),
aTimelineItemImageContent().copy(aspectRatio = 1.0f),
aTimelineItemImageContent().copy(aspectRatio = 1.5f),
)
}
fun aTimelineItemImageContent() = TimelineItemImageContent(
body = "a body",
imageMeta = MediaResolver.Meta(source = null, kind = MediaResolver.Kind.Content),
blurhash = null,
aspectRatio = 0.5f,
)

View file

@ -170,7 +170,7 @@ private fun aMessageEvent(
id = AN_EVENT_ID,
senderId = A_USER_ID.value,
senderDisplayName = A_USER_NAME,
senderAvatar = AvatarData(),
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
content = content,
sentTime = "",
isMine = isMine,

View file

@ -168,7 +168,7 @@ private fun aMessageEvent(
id = AN_EVENT_ID,
senderId = A_USER_ID.value,
senderDisplayName = A_USER_NAME,
senderAvatar = AvatarData(),
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
content = content,
sentTime = "",
isMine = isMine,