Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0a50063
Early setup for composable messages
ThomasSession Jun 9, 2025
ed6a32d
Composable message base setup and todos
ThomasSession Jun 9, 2025
9df1f96
Fixed colors
ThomasSession Jun 9, 2025
5123eb9
More work on message composables
ThomasSession Jun 10, 2025
7842d5e
Merge branch 'dev' into feature/compose-conversation
ThomasSession Jun 21, 2025
b8292c2
DocumentView
ThomasSession Jun 21, 2025
03198e5
Quotes
ThomasSession Jun 22, 2025
40ec5e6
quote images
ThomasSession Jun 22, 2025
36b6831
clean up
ThomasSession Jun 22, 2025
379e16e
Merge branch 'dev' into feature/compose-conversation
ThomasSession Jun 28, 2025
b7c64f3
Link data and UI in mesasges
ThomasSession Jun 30, 2025
6ec772d
Merge branch 'dev' into feature/compose-conversation
ThomasSession Jul 2, 2025
1ebd7fb
Using boxwithconstraint scope
ThomasSession Jul 3, 2025
ae7f9d6
Audio UI and using our new composable in the Landing page
ThomasSession Jul 3, 2025
4e7fd2a
More compose messaging ui
ThomasSession Jul 5, 2025
6fb76cf
Merge branch 'dev' into feature/compose-conversation
ThomasSession Jul 23, 2025
fb8ae93
Merge branch 'dev' into feature/compose-conversation
ThomasSession Jul 31, 2025
ebd05e2
WIP media messages
ThomasSession Jul 31, 2025
f5b9cdf
Media item sizing
ThomasSession Aug 4, 2025
8cc8d95
Merge branch 'dev' into feature/compose-conversation
ThomasSession Nov 3, 2025
a6d37f3
Merge branch 'dev' into feature/compose-conversation
ThomasSession Feb 19, 2026
4f4a2fc
Tweaks
ThomasSession Feb 19, 2026
62836fd
new audio player todo
ThomasSession Feb 19, 2026
b6d5557
Update app/src/main/java/org/thoughtcrime/securesms/conversation/v3/c…
ThomasSession Feb 19, 2026
907789d
Update app/src/main/java/org/thoughtcrime/securesms/conversation/v3/c…
ThomasSession Feb 19, 2026
c09bbc9
Update app/src/main/java/org/thoughtcrime/securesms/conversation/v3/c…
ThomasSession Feb 19, 2026
8f4fee1
Update app/src/main/java/org/thoughtcrime/securesms/conversation/v3/c…
ThomasSession Feb 19, 2026
d71c3b9
Update app/src/main/java/org/thoughtcrime/securesms/conversation/v3/c…
ThomasSession Feb 19, 2026
20c6c46
Merge branch 'dev' into feature/compose-conversation
ThomasSession Feb 20, 2026
1fff43b
PR feedback + compose audio player
ThomasSession Feb 20, 2026
570c4bc
Removing Glide from the new Composables
ThomasSession Feb 20, 2026
c0c671a
Merge branch 'dev' into feature/compose-conversation
ThomasSession Feb 20, 2026
88e90db
WIP reactions
ThomasSession Feb 22, 2026
b466988
emoji reactions cleaned up
ThomasSession Feb 23, 2026
cf640f7
Updated audio player look
ThomasSession Feb 23, 2026
70fbc3e
Merge branch 'dev' into feature/compose-conversation
ThomasSession Feb 23, 2026
b38daaf
Integrating the reactions
ThomasSession Feb 23, 2026
74fe2cd
Fixed preview
ThomasSession Feb 23, 2026
de9752c
Adding a new highlight effect. Going with a full row highlight instea…
ThomasSession Feb 23, 2026
8251f18
Status color
ThomasSession Feb 23, 2026
c91d7fc
Merge branch 'dev' into feature/compose-conversation
ThomasSession Feb 24, 2026
9f378b8
Reworking the message highlights
ThomasSession Feb 24, 2026
fd4cf64
Removing test data
ThomasSession Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,6 @@ data class OpenGroup(

companion object {

fun getServer(urlAsString: String): HttpUrl? {
val url = urlAsString.toHttpUrlOrNull() ?: return null
val builder = HttpUrl.Builder().scheme(url.scheme).host(url.host)
if (url.port != 80 || url.port != 443) {
// Non-standard port; add to server
builder.port(url.port)
}
return builder.build()
}

/**
* Returns the group ID for this community info. The group ID is the session android unique
* way of identifying a community. It itself isn't super useful but it's used to construct
Expand All @@ -53,16 +43,5 @@ data class OpenGroup(
}
}

fun toCommunityInfo(): BaseCommunityInfo {
return BaseCommunityInfo(
baseUrl = server,
room = room,
pubKeyHex = publicKey,
)
}


val joinURL: String get() = "$server/$room?public_key=$publicKey"

val groupId: String get() = "$server.$room"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package org.thoughtcrime.securesms.conversation.v3.compose

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle
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.DpSize
import androidx.compose.ui.unit.dp
import network.loki.messenger.R
import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors

private val playPauseSize = 36.dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AudioMessage(
data: Audio,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(vertical = LocalDimensions.current.xxsSpacing),

) {

val textColor = getTextColor(data.outgoing)

val (color1, color2, trackEmptyColor) = if (data.outgoing) {
arrayOf(
LocalColors.current.backgroundSecondary, // bg secondary
LocalColors.current.text, // text primary
LocalColors.current.backgroundSecondary.copy(alpha = 0.5f)
)
} else {
arrayOf(
LocalColors.current.accent, // accent
LocalColors.current.background, // background primary
LocalColors.current.textSecondary // text secondary

)
}

val playPauseSpacing = LocalDimensions.current.smallSpacing + playPauseSize + LocalDimensions.current.smallSpacing // aligns with slider start after play button

// Title
Text(
modifier = Modifier
.padding(start = playPauseSpacing, end = LocalDimensions.current.smallSpacing),
text = data.title,
style = LocalType.current.small.copy(fontStyle = FontStyle.Italic),
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)

// play + seek
Row(
modifier = Modifier
.padding(horizontal = LocalDimensions.current.xsSpacing),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing)
) {
PlayPauseButton(
isPlaying = data.isPlaying,
showLoader = data.showLoader,
bgColor = color1,
iconColor = color2,
onClick = {
//todo CONVOV3 implement
}
)

// Slider acts like SeekBar
val progress =
if (data.durationMs > 0) (data.positionMs.toFloat() / data.durationMs.toFloat())
else 0f

Slider(
modifier = Modifier.weight(1f),
value = progress.coerceIn(0f, 1f),
onValueChange = {
//todo CONVOV3 implement
},
enabled = !data.showLoader,
valueRange = 0f..1f,
thumb = { source ->
SliderDefaults.Thumb(
interactionSource = remember { MutableInteractionSource() },
colors = SliderDefaults.colors(thumbColor = color1),
thumbSize = DpSize(4.dp, 20.dp)
)
},
track = { sliderState ->
SliderDefaults.Track(
sliderState = sliderState,
modifier = Modifier.height(10.dp),
drawStopIndicator = null,
thumbTrackGapSize = 2.dp,
colors = SliderDefaults.colors(
activeTrackColor = color1,
inactiveTrackColor = trackEmptyColor
)
)
}
)
}

// Bottom: speed chip + remaining
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
start = playPauseSpacing,
end = LocalDimensions.current.smallSpacing
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
PlaybackSpeedButton(
text = data.speedText,
bgColor = if (data.outgoing) color1 else color2,
textColor = if(data.outgoing) color2 else textColor,
onClick = {
//todo CONVOV3 implement
}
)

Text(
text = data.remainingText,
style = LocalType.current.small,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}

@Composable
private fun PlayPauseButton(
isPlaying: Boolean,
showLoader: Boolean,
bgColor: Color,
iconColor: Color,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {

Box(
modifier = modifier
.size(playPauseSize)
.clip(CircleShape)
.background(bgColor)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
if (showLoader) {
SmallCircularProgressIndicator(color = iconColor)
} else {
Image(
painter = painterResource(
id = if (isPlaying) R.drawable.pause else R.drawable.play
),
contentDescription = null,
colorFilter = ColorFilter.tint(iconColor),
modifier = Modifier.size(16.dp)
)
}
}
}


@Composable
private fun PlaybackSpeedButton(
text: String,
bgColor: Color,
textColor: Color,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(LocalDimensions.current.shapeXXSmall))
.background(bgColor)
.clickable(onClick = onClick)
.padding(
horizontal = LocalDimensions.current.xxsSpacing,
vertical = LocalDimensions.current.xxxsSpacing
),
contentAlignment = Alignment.Center
) {
Text(
text = text,
style = LocalType.current.small,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Clip
)
}
}

data class Audio(
override val outgoing: Boolean,
override val text: AnnotatedString? = null,
val title: String,
val speedText: String,
val remainingText: String,
val durationMs: Long, // slider max reference
val positionMs: Long, // slider position
val bufferedPositionMs: Long = 0L,
val isPlaying: Boolean,
val showLoader: Boolean,
) : MessageType()

@Preview
@Composable
fun AudioMessagePreview(
@PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
) {
PreviewTheme(colors) {
Column(
modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing)

) {
Message(data = MessageViewData(
author = "Toto",
type = PreviewMessageData.audio()
))

Spacer(modifier = Modifier.height(LocalDimensions.current.spacing))

Message(data = MessageViewData(
author = "Toto",
avatar = PreviewMessageData.sampleAvatar,
type = PreviewMessageData.audio(
outgoing = false,
title = "Audio with a really long name that should ellipsize once it reaches the max width",
)
))

Spacer(modifier = Modifier.height(LocalDimensions.current.spacing))

Message(data = MessageViewData(
author = "Toto",
type = PreviewMessageData.audio(
playing = false
)
))
}
}
}
Loading