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,