diff --git a/apps/flipcash/core/src/main/res/drawable/ic_existing_contact.xml b/apps/flipcash/core/src/main/res/drawable/ic_existing_contact.xml
new file mode 100644
index 000000000..fde3688da
--- /dev/null
+++ b/apps/flipcash/core/src/main/res/drawable/ic_existing_contact.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/apps/flipcash/core/src/main/res/drawable/ic_unknown_contact.xml b/apps/flipcash/core/src/main/res/drawable/ic_unknown_contact.xml
new file mode 100644
index 000000000..d62db27fb
--- /dev/null
+++ b/apps/flipcash/core/src/main/res/drawable/ic_unknown_contact.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml
index dfd004b9c..ecd480a79 100644
--- a/apps/flipcash/core/src/main/res/values/strings.xml
+++ b/apps/flipcash/core/src/main/res/values/strings.xml
@@ -771,5 +771,6 @@
Delivered
Read %1$s
Yesterday
+ Today
\ No newline at end of file
diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
index 24eb618fd..d2754eace 100644
--- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
+++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
@@ -263,7 +263,7 @@ internal class SendFlowViewModel @Inject constructor(
val formattedPhone = phone?.let { phoneUtils.formatNumber(it) }
val displayName = otherMember.userProfile.displayName?.takeIf { it.isNotBlank() }
?: formattedPhone
- ?: "Unknown Contact"
+ ?: return@mapNotNull null // filter out anonymous chats
val unknown = DeviceContact.unknownContact(
e164 = phone.orEmpty(),
diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt
index 2c9945d9d..95338feb1 100644
--- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt
+++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt
@@ -315,30 +315,12 @@ private fun ContactRowItem(
} else {
Box(modifier = Modifier.requiredWidth(CodeTheme.dimens.inset))
}
- if (isNonContactDm && contact.photoUri == null) {
- Box(
- modifier = Modifier
- .requiredSize(CodeTheme.dimens.staticGrid.x8)
- .clip(CircleShape)
- .background(Brush.linearGradient(CodeTheme.colors.contactAvatar.colors)),
- contentAlignment = Alignment.Center,
- ) {
- Icon(
- imageVector = Icons.Default.Person,
- contentDescription = null,
- tint = CodeTheme.colors.textSecondary,
- modifier = Modifier.size(CodeTheme.dimens.staticGrid.x5),
- )
- }
- } else {
- ContactAvatar(
- photoUri = contact.photoUri,
- displayName = contact.displayName,
- modifier = Modifier
- .requiredSize(CodeTheme.dimens.staticGrid.x8)
- .clip(CircleShape),
- )
- }
+ ContactAvatar(
+ contact = contact,
+ modifier = Modifier
+ .requiredSize(CodeTheme.dimens.staticGrid.x8)
+ .clip(CircleShape),
+ )
}
Column(modifier = Modifier.weight(1f)) {
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt
index 019a936a8..61a4e8e4e 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt
@@ -44,7 +44,6 @@ import com.getcode.opencode.model.financial.Limits
import com.getcode.opencode.model.financial.SendLimit
import com.getcode.opencode.model.financial.Token
import com.getcode.solana.keys.PublicKey
-import com.getcode.util.DateUtils
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.trace
import com.getcode.view.BaseViewModel
@@ -205,7 +204,7 @@ internal class ChatViewModel @Inject constructor(
}.insertSeparators { before: ChatListItem.ContentBubble?, after: ChatListItem.ContentBubble? ->
if (before == null) return@insertSeparators null
if (after == null || separatorConfig.shouldSeparate(before.timestamp, after.timestamp)) {
- ChatListItem.DateSeparator(formatDateLabel(before.timestamp))
+ ChatListItem.DateSeparator(before.timestamp)
} else null
}
}.cachedIn(viewModelScope)
@@ -616,10 +615,6 @@ internal class ChatViewModel @Inject constructor(
}
companion object {
- private fun formatDateLabel(instant: Instant): String {
- return DateUtils.getDateWithToday(instant.toEpochMilliseconds())
- }
-
val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
when (event) {
is Event.OnChatOpened -> { state -> state }
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt
index 61199c13e..bd8a7660a 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt
@@ -12,6 +12,7 @@ import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -24,8 +25,12 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -37,6 +42,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
@@ -91,6 +97,7 @@ internal fun MessengerScreen(viewModel: ChatViewModel) {
modifier = Modifier
.fillMaxSize()
.hazeSource(hazeState),
+ state = state,
contentPadding = overlapPadding,
messages = messages,
separatorConfig = SeparatorConfig.TimeGap(),
@@ -119,44 +126,34 @@ private fun ChatTopBar(
endY = { size.height * 0.25f }
)
)
- AppBarWithTitle(
- modifier = Modifier.measured { titleHeight = it.height },
- leftIcon = {
- AppBarDefaults.UpNavigation { navigator.pop() }
- },
- rightContents = {
- AppBarDefaults.Overflow {
- // TODO:
- }
- },
- title = {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
- ) {
- ContactAvatar(
- modifier = Modifier
- .border(
- CodeTheme.dimens.border,
- CodeTheme.colors.divider,
- CircleShape
- )
- .size(CodeTheme.dimens.staticGrid.x8)
- .clip(CircleShape),
- photoUri = chattingWith?.photoUri,
- displayName = chattingWith?.displayName.orEmpty(),
- )
- Text(
- modifier = Modifier.weight(1f),
- text = chattingWith?.displayName.orEmpty(),
- style = CodeTheme.typography.textMedium,
- color = CodeTheme.colors.textMain,
- )
+ AppBarWithTitle(
+ modifier = Modifier.measured { titleHeight = it.height },
+ leftIcon = {
+ AppBarDefaults.UpNavigation { navigator.pop() }
+ },
+ title = {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
+ ) {
+ ContactAvatar(
+ contact = chattingWith,
+ modifier = Modifier
+ .requiredSize(CodeTheme.dimens.staticGrid.x8)
+ .clip(CircleShape),
+ )
+
+ Text(
+ modifier = Modifier.weight(1f),
+ text = chattingWith?.displayName.orEmpty(),
+ style = CodeTheme.typography.textMedium,
+ color = CodeTheme.colors.textMain,
+ )
+ }
}
- }
- )
- }
+ )
+ }
}
@Composable
@@ -222,81 +219,74 @@ private fun UserControlBottomBar(
.padding(vertical = CodeTheme.dimens.grid.x3)
.navigationBarsPadding(),
targetState = state.userState,
- transitionSpec = {
- when (targetState) {
- ChatViewModel.UserState.Typing ->
- slideInVertically { it } + fadeIn() togetherWith fadeOut()
+ transitionSpec = {
+ when (targetState) {
+ ChatViewModel.UserState.Typing ->
+ slideInVertically { it } + fadeIn() togetherWith fadeOut()
- ChatViewModel.UserState.Reading ->
- fadeIn() togetherWith slideOutVertically { it } + fadeOut()
- }
- },
- ) { s ->
- when (s) {
- ChatViewModel.UserState.Reading -> {
- val isUnknownContact = state.chattingWith?.isUnknown == true || state.chattingWith == null
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
- ) {
- AnimatedVisibility(
- visible = !isUnknownContact,
- modifier = Modifier.weight(1f),
- enter = expandHorizontally(expandFrom = Alignment.Start) + fadeIn(),
- exit = shrinkHorizontally(shrinkTowards = Alignment.Start) + fadeOut(),
+ ChatViewModel.UserState.Reading ->
+ fadeIn() togetherWith slideOutVertically { it } + fadeOut()
+ }
+ },
+ ) { s ->
+ when (s) {
+ ChatViewModel.UserState.Reading -> {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
) {
CodeButton(
modifier = Modifier.weight(1f),
buttonState = ButtonState.Filled,
text = stringResource(R.string.action_sendCash),
) { dispatch(ChatViewModel.Event.OnSendCash) }
- }
- AnimatedVisibility(
- visible = state.typingConstraints.enabled,
- modifier = Modifier.weight(1f),
- enter = expandHorizontally(expandFrom = Alignment.Start) + fadeIn(),
- exit = shrinkHorizontally(shrinkTowards = Alignment.Start) + fadeOut(),
- ) {
- CodeButton(
- modifier = Modifier
- .hazeEffect(hazeState) {
- blurEffect {
- style = material
- }
- },
- buttonState = ButtonState.Filled10,
- text = stringResource(R.string.action_sendMessage),
- ) { dispatch(ChatViewModel.Event.OnStartMessageInput) }
+ AnimatedVisibility(
+ visible = state.typingConstraints.enabled,
+ modifier = Modifier.weight(1f),
+ enter = expandHorizontally(expandFrom = Alignment.Start) + fadeIn(),
+ exit = shrinkHorizontally(shrinkTowards = Alignment.Start) + fadeOut(),
+ ) {
+ CodeButton(
+ modifier = Modifier
+ .hazeEffect(hazeState) {
+ blurEffect {
+ style = material
+ }
+ },
+ buttonState = ButtonState.Filled10,
+ text = stringResource(R.string.action_sendMessage),
+ ) { dispatch(ChatViewModel.Event.OnStartMessageInput) }
+ }
}
}
- }
- ChatViewModel.UserState.Typing -> {
- ChatInput(
- modifier = Modifier
- .fillMaxWidth()
- .border(
- CodeTheme.dimens.border,
- CodeTheme.colors.divider,
- CodeTheme.shapes.medium,
- ).hazeEffect(hazeState) {
- blurEffect {
- style = material
- }
- },
- focusRequester = focusRequester,
- hint = "Message",
- state = state.chatInputState,
- onSendMessage = { dispatch(ChatViewModel.Event.SendMessage) },
- )
+ ChatViewModel.UserState.Typing -> {
+ ChatInput(
+ modifier = Modifier
+ .fillMaxWidth()
+ .border(
+ CodeTheme.dimens.border,
+ CodeTheme.colors.divider,
+ CodeTheme.shapes.medium,
+ )
+ .hazeEffect(hazeState) {
+ blurEffect {
+ style = material
+ }
+ },
+ focusRequester = focusRequester,
+ hint = "Message",
+ state = state.chatInputState,
+ onSendMessage = { dispatch(ChatViewModel.Event.SendMessage) },
+ )
- LaunchedEffect(Unit) {
- focusRequester.requestFocus()
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
}
}
}
}
- }
}
}
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt
new file mode 100644
index 000000000..f587e0821
--- /dev/null
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt
@@ -0,0 +1,118 @@
+package com.flipcash.app.messenger.internal.screens.components
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+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.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.TextAutoSize
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import com.flipcash.app.contacts.device.DeviceContact
+import com.flipcash.app.contacts.ui.ContactAvatar
+import com.flipcash.features.messenger.R
+import com.getcode.theme.CodeTheme
+
+@Composable
+internal fun ContactInfoContainer(
+ contact: DeviceContact?,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .border(
+ color = CodeTheme.colors.divider,
+ width = CodeTheme.dimens.border,
+ shape = CodeTheme.shapes.medium,
+ )
+ .padding(CodeTheme.dimens.grid.x6),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ ContactAvatar(
+ contact = contact,
+ modifier = Modifier
+ .size(CodeTheme.dimens.staticGrid.x17)
+ .clip(CircleShape),
+ )
+ Text(
+ modifier = Modifier.padding(top = CodeTheme.dimens.grid.x2),
+ text = contact?.displayName.orEmpty(),
+ autoSize = TextAutoSize.StepBased(
+ minFontSize = CodeTheme.typography.textSmall.fontSize,
+ maxFontSize = CodeTheme.typography.textLarge.fontSize,
+ ),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = CodeTheme.typography.textLarge,
+ color = CodeTheme.colors.textMain,
+ )
+
+ ContactPill(
+ modifier = Modifier.padding(top = CodeTheme.dimens.inset),
+ contact = contact
+ )
+ }
+}
+
+@Composable
+private fun ContactPill(
+ contact: DeviceContact?,
+ modifier: Modifier = Modifier
+) {
+ AnimatedContent(contact) { c ->
+ if (c == null) {
+ Spacer(modifier = modifier.fillMaxWidth())
+ return@AnimatedContent
+ }
+
+ val isContact = !c.isUnknown
+ val backgroundColor by animateColorAsState(
+ if (isContact) CodeTheme.colors.surfaceVariant else CodeTheme.colors.warning.copy(alpha = 0.10f)
+ )
+
+ val contentColor by animateColorAsState(
+ if (isContact) CodeTheme.colors.textSecondary else CodeTheme.colors.warning
+ )
+
+ Row(
+ modifier = modifier
+ .background(color = backgroundColor, shape = CircleShape)
+ .padding(horizontal = CodeTheme.dimens.grid.x2, vertical = CodeTheme.dimens.grid.x1),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1)
+ ) {
+ Icon(
+ modifier = Modifier.size(CodeTheme.dimens.staticGrid.x4),
+ painter = painterResource(
+ if (isContact) {
+ R.drawable.ic_existing_contact
+ } else {
+ R.drawable.ic_unknown_contact
+ }
+ ),
+ contentDescription = null,
+ tint = contentColor,
+ )
+
+ Text(
+ text = if (isContact) "From Your Contacts" else "Unknown Contact",
+ color = contentColor,
+ style = CodeTheme.typography.textSmall,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/DateSeparator.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/DateSeparator.kt
new file mode 100644
index 000000000..21b1524f3
--- /dev/null
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/DateSeparator.kt
@@ -0,0 +1,82 @@
+package com.flipcash.app.messenger.internal.screens.components
+
+import android.text.format.DateFormat
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.flipcash.features.messenger.R
+import com.getcode.theme.CodeTheme
+import com.getcode.util.formatLocalized
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import androidx.compose.foundation.layout.Column
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewWrapper
+import com.flipcash.app.theme.FlipcashThemeWrapper
+import kotlin.time.Clock
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Instant
+
+@Composable
+internal fun DateSeparatorRow(
+ timestamp: Instant,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = CodeTheme.dimens.grid.x2),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = formatDateSeparator(timestamp),
+ style = CodeTheme.typography.caption,
+ color = CodeTheme.colors.textSecondary,
+ )
+ }
+}
+
+@Composable
+private fun formatDateSeparator(instant: Instant): String {
+ val context = LocalContext.current
+ val is24Hour = DateFormat.is24HourFormat(context)
+ val tz = TimeZone.currentSystemDefault()
+ val todayDate = Clock.System.now().toLocalDateTime(tz).date
+ val messageDate = instant.toLocalDateTime(tz).date
+ val dayDiff = todayDate.toEpochDays() - messageDate.toEpochDays()
+
+ val time = instant.formatLocalized("h:mm a", is24Hour = is24Hour, if24Hour = "H:mm")
+
+ return when {
+ dayDiff == 0L -> "${stringResource(R.string.label_chatSeparator_today)} $time"
+ dayDiff == 1L -> "${stringResource(R.string.label_chatReceipt_yesterday)} $time"
+ dayDiff in 2L..6L -> "${instant.formatLocalized("EEEE")} $time"
+ messageDate.year == todayDate.year -> "${instant.formatLocalized("MMM d")} $time"
+ else -> "${instant.formatLocalized("MMM d, yyyy")} $time"
+ }
+}
+
+// region Previews
+
+@Preview
+@PreviewWrapper(FlipcashThemeWrapper::class)
+@Composable
+private fun Preview_AllRanges() {
+ val now = Clock.System.now()
+ Column {
+ DateSeparatorRow(timestamp = now.minus(2.hours))
+ DateSeparatorRow(timestamp = now.minus(1.days))
+ DateSeparatorRow(timestamp = now.minus(3.days))
+ DateSeparatorRow(timestamp = now.minus(30.days))
+ DateSeparatorRow(timestamp = now.minus(400.days))
+ }
+}
+
+// endregion
\ No newline at end of file
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageBubble.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageBubble.kt
index 4f0a0091a..631e49ecf 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageBubble.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageBubble.kt
@@ -239,25 +239,6 @@ internal fun bubblePositionOf(
}
}
-@Composable
-internal fun DateSeparatorRow(
- label: String,
- modifier: Modifier = Modifier,
-) {
- Box(
- modifier = modifier
- .fillMaxWidth()
- .padding(vertical = CodeTheme.dimens.grid.x2),
- contentAlignment = Alignment.Center,
- ) {
- Text(
- text = label,
- style = CodeTheme.typography.caption,
- color = CodeTheme.colors.textSecondary,
- )
- }
-}
-
// region Previews
@Preview
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt
index c7c18859f..d3b1af726 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt
@@ -1,15 +1,19 @@
package com.flipcash.app.messenger.internal.screens.components
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -21,10 +25,13 @@ import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemKey
+import com.flipcash.app.contacts.ui.ContactAvatar
+import com.flipcash.app.messenger.internal.ChatViewModel
import com.flipcash.services.models.chat.MessageContent
import com.flipcash.services.models.chat.MessagePointer
import com.getcode.theme.CodeTheme
@@ -67,8 +74,8 @@ internal enum class ReceiptStatus { SENDING, SENT, READ, FAILED }
internal sealed interface ChatListItem {
val itemKey: Any
- data class DateSeparator(val label: String) : ChatListItem {
- override val itemKey: Any = "sep-$label"
+ data class DateSeparator(val timestamp: kotlin.time.Instant) : ChatListItem {
+ override val itemKey: Any = "sep-${timestamp.epochSeconds}"
}
data class ContentBubble(
@@ -88,16 +95,12 @@ internal sealed interface ChatListItem {
internal fun MessageList(
modifier: Modifier = Modifier,
contentPadding: PaddingValues,
+ state: ChatViewModel.State,
messages: LazyPagingItems,
separatorConfig: SeparatorConfig,
otherReadPointer: MessagePointer? = null,
onAdvanceReadPointer: ((Long) -> Unit)? = null,
) {
- val listAlpha by animateFloatAsState(
- targetValue = if (messages.itemCount > 0) 1f else 0f,
- label = "messageListAlpha",
- )
-
val listState = rememberLazyListState()
if (onAdvanceReadPointer != null) {
@@ -106,12 +109,11 @@ internal fun MessageList(
LazyColumn(
modifier = modifier
- .alpha(listAlpha)
.sheetResignmentBehavior(listState),
state = listState,
reverseLayout = true,
contentPadding = PaddingValues(
- top = CodeTheme.dimens.grid.x2 + contentPadding.calculateTopPadding(),
+ top = CodeTheme.dimens.inset + contentPadding.calculateTopPadding(),
bottom = CodeTheme.dimens.grid.x2 + contentPadding.calculateBottomPadding(),
start = CodeTheme.dimens.inset,
end = CodeTheme.dimens.inset,
@@ -131,7 +133,7 @@ internal fun MessageList(
.animateItem(placementSpec = null),
) {
when (item) {
- is ChatListItem.DateSeparator -> DateSeparatorRow(item.label)
+ is ChatListItem.DateSeparator -> DateSeparatorRow(item.timestamp)
is ChatListItem.ContentBubble -> {
val effectiveStatus = effectiveReceiptStatus(item, otherReadPointer)
Column(
@@ -152,6 +154,17 @@ internal fun MessageList(
}
}
}
+
+ // Chat start shows contact info container
+ item {
+ Box(modifier = Modifier.fillParentMaxWidth(), contentAlignment = Alignment.Center) {
+ ContactInfoContainer(
+ contact = state.chattingWith,
+ modifier = Modifier
+ .padding(horizontal = CodeTheme.dimens.grid.x12)
+ )
+ }
+ }
}
// opts out of the list maintaining
diff --git a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ui/ContactAvatar.kt b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ui/ContactAvatar.kt
index 7de0c6be2..4cb3c5292 100644
--- a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ui/ContactAvatar.kt
+++ b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ui/ContactAvatar.kt
@@ -1,8 +1,17 @@
package com.flipcash.app.contacts.ui
+import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.BoxWithConstraintsScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -12,15 +21,61 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.min
import androidx.core.net.toUri
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
+import com.flipcash.app.contacts.device.DeviceContact
import com.getcode.theme.CodeTheme
+@Composable
+fun ContactAvatar(
+ contact: DeviceContact?,
+ modifier: Modifier = Modifier,
+) {
+ if (contact == null || contact.isUnknown) {
+ Box(
+ modifier = modifier
+ .background(Brush.linearGradient(CodeTheme.colors.contactAvatar.colors)),
+ contentAlignment = Alignment.BottomCenter,
+ ) {
+ Image(
+ imageVector = Icons.Default.Person,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(CodeTheme.colors.textSecondary),
+ modifier = Modifier
+ .fillMaxSize()
+ .graphicsLayer {
+ val scale = 1.2f
+ scaleX = scale
+ scaleY = scale
+ translationY = size.height * 0.18f
+ },
+ contentScale = ContentScale.Fit,
+ )
+ }
+ } else {
+ ContactAvatar(
+ modifier = Modifier
+ .border(
+ CodeTheme.dimens.border,
+ CodeTheme.colors.divider,
+ CircleShape,
+ ).then(modifier),
+ photoUri = contact.photoUri,
+ displayName = contact.displayName,
+ )
+ }
+}
@Composable
fun ContactAvatar(
@@ -28,7 +83,7 @@ fun ContactAvatar(
displayName: String,
modifier: Modifier = Modifier,
) {
- Box(
+ BoxWithConstraints(
modifier = modifier.background(
Brush.linearGradient(CodeTheme.colors.contactAvatar.colors)
)
@@ -60,7 +115,7 @@ fun ContactAvatar(
}
@Composable
-private fun BoxScope.InitialsText(displayName: String) {
+private fun BoxWithConstraintsScope.InitialsText(displayName: String) {
val initials = remember(displayName) {
displayName.split(" ")
.take(2)
@@ -68,10 +123,13 @@ private fun BoxScope.InitialsText(displayName: String) {
.joinToString("")
.ifEmpty { "?" }
}
+ val fontSize = with(LocalDensity.current) {
+ (min(maxWidth, maxHeight) * 0.38f).toSp()
+ }
Text(
modifier = Modifier.align(Alignment.Center),
text = initials,
- style = CodeTheme.typography.textSmall,
+ fontSize = fontSize,
color = CodeTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
diff --git a/apps/flipcash/shared/theme/src/main/kotlin/com/flipcash/app/theme/internal/FlipcashDesignSystem.kt b/apps/flipcash/shared/theme/src/main/kotlin/com/flipcash/app/theme/internal/FlipcashDesignSystem.kt
index f8aa8fde3..98507b987 100644
--- a/apps/flipcash/shared/theme/src/main/kotlin/com/flipcash/app/theme/internal/FlipcashDesignSystem.kt
+++ b/apps/flipcash/shared/theme/src/main/kotlin/com/flipcash/app/theme/internal/FlipcashDesignSystem.kt
@@ -37,6 +37,7 @@ object Flipcash2ColorSpec {
val trackColor = Color.White.copy(alpha = 0.07f)
val bannerThemed = Color(0xFF252526)
val success = Color(0xFF1AC86A)
+ val warning = Color(0xFFFFA953)
val successText = Color(0xFF73EAA4)
val surfaceVariant = Color.White.copy(alpha = 0.12f)
val errorSurface = Color(0x4AE75454)
@@ -102,6 +103,7 @@ private val colors = with(Flipcash2ColorSpec) {
surfaceVariant = surfaceVariant,
surfaceError = errorSurface,
onSurface = White,
+ warning = warning,
error = Error,
errorText = TextError,
success = success,
@@ -119,7 +121,7 @@ private val colors = with(Flipcash2ColorSpec) {
betaIndicator = BetaIndicator,
bannerThemed = bannerThemed,
bannerError = Error,
- bannerWarning = Warning,
+ bannerWarning = warning,
bannerSuccess = BannerSuccess,
scrim = Black40,
accessKey = accessKey,
diff --git a/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt b/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt
index 66935063e..f54251480 100644
--- a/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt
+++ b/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt
@@ -37,6 +37,7 @@ internal val CodeDefaultColorScheme = ColorScheme(
surfaceVariant = BrandDark,
surfaceError = Error,
onSurface = White,
+ warning = Warning,
error = Error,
errorText = TextError,
success = Success,
@@ -140,6 +141,7 @@ class ColorScheme(
border: Color,
divider: Color,
dividerVariant: Color,
+ warning: Color,
error: Color,
errorText: Color,
success: Color,
@@ -187,6 +189,8 @@ class ColorScheme(
private set
var onSurface by mutableStateOf(onSurface)
private set
+ var warning by mutableStateOf(warning)
+ private set
var error by mutableStateOf(error)
private set
var errorText by mutableStateOf(errorText)
@@ -261,6 +265,7 @@ class ColorScheme(
surfaceVariant = other.surfaceVariant
surfaceError = other.surfaceError
onSurface = other.onSurface
+ warning = other.warning
error = other.error
errorText = other.errorText
success = other.success
@@ -305,6 +310,7 @@ class ColorScheme(
surfaceVariant = surfaceVariant,
surfaceError = surfaceError,
onSurface = onSurface,
+ warning = warning,
error = error,
errorText = errorText,
success = success,