From 09c9bf6bd909e84e3bf9d6a9f65a822b24112224 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Mon, 4 May 2026 22:31:42 +0530 Subject: [PATCH 1/4] feat: show relative date labels in chat separators Display Today, Yesterday, and weekday names for recent messages instead of raw dates in date separators. Closes #87 --- .../chats/conversation/ui/ChatDateLabel.kt | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatDateLabel.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatDateLabel.kt index 0b7f628f..2b80425f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatDateLabel.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatDateLabel.kt @@ -17,11 +17,34 @@ fun isTodayTimestamp(timestamp: Int, locale: Locale = Locale.getDefault()): Bool fun formatChatDayLabel(timestamp: Int, locale: Locale = Locale.getDefault()): String { val date = Date(timestamp.toLong() * 1000) - val calendar = Calendar.getInstance(locale) - val currentYear = calendar.get(Calendar.YEAR) - calendar.time = date - val messageYear = calendar.get(Calendar.YEAR) + val now = Calendar.getInstance(locale) + val msgCal = Calendar.getInstance(locale).apply { time = date } + + val nowYear = now.get(Calendar.YEAR) + val nowDay = now.get(Calendar.DAY_OF_YEAR) + val msgYear = msgCal.get(Calendar.YEAR) + val msgDay = msgCal.get(Calendar.DAY_OF_YEAR) + + if (nowYear == msgYear) { + val dayDiff = nowDay - msgDay + return when { + dayDiff == 0 -> "Today" + dayDiff == 1 -> "Yesterday" + dayDiff in 2..6 -> SimpleDateFormat("EEEE", locale).format(date) + else -> SimpleDateFormat("d MMMM", locale).format(date) + } + } + + if (nowYear - msgYear == 1 && msgDay >= 359 && nowDay <= 6) { + val daysInMsgYear = msgCal.getActualMaximum(Calendar.DAY_OF_YEAR) + val dayDiff = (daysInMsgYear - msgDay) + nowDay + return when { + dayDiff == 0 -> "Today" + dayDiff == 1 -> "Yesterday" + dayDiff in 2..6 -> SimpleDateFormat("EEEE", locale).format(date) + else -> SimpleDateFormat("d MMMM yyyy", locale).format(date) + } + } - val pattern = if (messageYear == currentYear) "d MMMM" else "d MMMM yyyy" - return SimpleDateFormat(pattern, locale).format(date) + return SimpleDateFormat("d MMMM yyyy", locale).format(date) } From f655b9bcf260fc65bd525ede401b014df5465a6c Mon Sep 17 00:00:00 2001 From: Nikhil Date: Mon, 4 May 2026 22:31:53 +0530 Subject: [PATCH 2/4] feat: allow creating groups without selecting members Remove member selection gate to enable single-member group creation via TDLib's createBasicGroupChat API. Closes #149 --- .../chats/creation/DefaultNewChatComponent.kt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/creation/DefaultNewChatComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/DefaultNewChatComponent.kt index 455f023b..4e477873 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/creation/DefaultNewChatComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/DefaultNewChatComponent.kt @@ -225,15 +225,13 @@ class DefaultNewChatComponent( when (currentState.step) { NewChatComponent.Step.GROUP_MEMBERS -> { - if (currentState.selectedUserIds.isNotEmpty()) { - _state.update { - it.copy( - step = NewChatComponent.Step.GROUP_INFO, - searchQuery = "", - searchResults = emptyList(), - validationError = null - ) - } + _state.update { + it.copy( + step = NewChatComponent.Step.GROUP_INFO, + searchQuery = "", + searchResults = emptyList(), + validationError = null + ) } } From 84d93674fd31c18330e2edd3b0b236badf87f1a2 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Mon, 4 May 2026 22:32:03 +0530 Subject: [PATCH 3/4] feat: show date on forwarded messages from a different day Display date alongside time for forwards/reposts that originated on a different day (e.g. '3 May, 14:30' instead of just '14:30'). Closes #90 --- .../chats/conversation/ui/message/ForwardContent.kt | 10 +++++++++- .../chats/conversation/ui/message/MessageUtils.kt | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ForwardContent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ForwardContent.kt index 07363902..d05387dc 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ForwardContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ForwardContent.kt @@ -33,6 +33,7 @@ import org.monogram.domain.models.ForwardInfo import org.monogram.domain.models.ForwardOriginType import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.util.DateFormatManager +import org.monogram.presentation.features.chats.conversation.ui.isTodayTimestamp @Composable fun ForwardContent( @@ -132,9 +133,16 @@ fun ForwardContent( ) if (forwardInfo.date > 0) { + val forwardDateText = remember(forwardInfo.date, timeFormat) { + if (isTodayTimestamp(forwardInfo.date)) { + formatTime(forwardInfo.date, timeFormat) + } else { + formatForwardDate(forwardInfo.date, timeFormat) + } + } Spacer(modifier = Modifier.width(6.dp)) Text( - text = formatTime(forwardInfo.date, timeFormat), + text = forwardDateText, style = MaterialTheme.typography.labelSmall.copy(fontSize = 11.sp), color = contentColor.copy(alpha = 0.56f), maxLines = 1 diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageUtils.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageUtils.kt index 113730c3..b78a12e3 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageUtils.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageUtils.kt @@ -55,6 +55,7 @@ import org.monogram.presentation.features.chats.conversation.ui.channel.formatVi import java.io.File import java.text.BreakIterator import java.text.SimpleDateFormat +import java.util.Calendar import java.util.Date import java.util.Locale import kotlin.math.log10 @@ -67,6 +68,17 @@ val LocalLinkHandler = staticCompositionLocalOf<(String) -> Unit> { fun formatTime(ts: Int, timeFormat: String): String = SimpleDateFormat(timeFormat, Locale.getDefault()).format(Date(ts.toLong() * 1000)) +fun formatForwardDate(ts: Int, timeFormat: String): String { + val date = Date(ts.toLong() * 1000) + val locale = Locale.getDefault() + val cal = Calendar.getInstance(locale).apply { time = date } + val now = Calendar.getInstance(locale) + val datePattern = if (cal.get(Calendar.YEAR) == now.get(Calendar.YEAR)) "d MMM" else "d MMM yyyy" + val datePart = SimpleDateFormat(datePattern, locale).format(date) + val timePart = SimpleDateFormat(timeFormat, locale).format(date) + return "$datePart, $timePart" +} + fun formatDuration(seconds: Int): String { val m = seconds / 60 val s = seconds % 60 From f9be946bc12b492e844f7b658fe2ed0f177d3ba8 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Tue, 5 May 2026 22:09:47 +0530 Subject: [PATCH 4/4] fix: use localized string resources for date labels --- .../features/chats/conversation/ui/ChatDateLabel.kt | 12 +++++++----- .../features/chats/conversation/ui/DateSeparator.kt | 4 +++- .../chats/conversation/ui/content/ChatContentList.kt | 4 +++- presentation/src/main/res/values/string.xml | 2 ++ 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatDateLabel.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatDateLabel.kt index 2b80425f..14745fee 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatDateLabel.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatDateLabel.kt @@ -1,5 +1,7 @@ package org.monogram.presentation.features.chats.conversation.ui +import android.content.Context +import org.monogram.presentation.R import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -15,7 +17,7 @@ fun isTodayTimestamp(timestamp: Int, locale: Locale = Locale.getDefault()): Bool calendar.get(Calendar.DAY_OF_YEAR) == todayDayOfYear } -fun formatChatDayLabel(timestamp: Int, locale: Locale = Locale.getDefault()): String { +fun formatChatDayLabel(timestamp: Int, context: Context, locale: Locale = Locale.getDefault()): String { val date = Date(timestamp.toLong() * 1000) val now = Calendar.getInstance(locale) val msgCal = Calendar.getInstance(locale).apply { time = date } @@ -28,8 +30,8 @@ fun formatChatDayLabel(timestamp: Int, locale: Locale = Locale.getDefault()): St if (nowYear == msgYear) { val dayDiff = nowDay - msgDay return when { - dayDiff == 0 -> "Today" - dayDiff == 1 -> "Yesterday" + dayDiff == 0 -> context.getString(R.string.chat_date_today) + dayDiff == 1 -> context.getString(R.string.chat_date_yesterday) dayDiff in 2..6 -> SimpleDateFormat("EEEE", locale).format(date) else -> SimpleDateFormat("d MMMM", locale).format(date) } @@ -39,8 +41,8 @@ fun formatChatDayLabel(timestamp: Int, locale: Locale = Locale.getDefault()): St val daysInMsgYear = msgCal.getActualMaximum(Calendar.DAY_OF_YEAR) val dayDiff = (daysInMsgYear - msgDay) + nowDay return when { - dayDiff == 0 -> "Today" - dayDiff == 1 -> "Yesterday" + dayDiff == 0 -> context.getString(R.string.chat_date_today) + dayDiff == 1 -> context.getString(R.string.chat_date_yesterday) dayDiff in 2..6 -> SimpleDateFormat("EEEE", locale).format(date) else -> SimpleDateFormat("d MMMM yyyy", locale).format(date) } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/DateSeparator.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/DateSeparator.kt index 36efa6df..0f497119 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/DateSeparator.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/DateSeparator.kt @@ -10,11 +10,13 @@ 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.unit.dp @Composable fun DateSeparator(timestamp: Int) { - val text = formatChatDayLabel(timestamp) + val context = LocalContext.current + val text = formatChatDayLabel(timestamp, context) Box( modifier = Modifier .fillMaxWidth() diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt index 5be70b77..7d9a7522 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt @@ -73,6 +73,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription @@ -191,6 +192,7 @@ fun ChatContentList( ) { val isComments = state.isComments val appearance = state.toAppearanceConfig() + val context = LocalContext.current val density = LocalDensity.current val isScrolling by remember(scrollState) { derivedStateOf { scrollState.isScrollInProgress } } val latestState by rememberUpdatedState(state) @@ -306,7 +308,7 @@ fun ChatContentList( viewportTopOffset = viewportTopOffset ) ?: return@derivedStateOf null - formatChatDayLabel(anchor.mainTimestamp()) + formatChatDayLabel(anchor.mainTimestamp(), context) } } diff --git a/presentation/src/main/res/values/string.xml b/presentation/src/main/res/values/string.xml index 352ffd67..03454849 100644 --- a/presentation/src/main/res/values/string.xml +++ b/presentation/src/main/res/values/string.xml @@ -2102,6 +2102,8 @@ Unsupported message + Today + Yesterday %1$02d:%2$02d