diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 682e3f335..cbd83d58a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -1624,8 +1624,9 @@ fun getTimestampText(t: Instant): String { val tz = TimeZone.currentSystemDefault() val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz) val time: LocalDateTime = t.toLocalDateTime(tz) + val period = now.date.minus(time.date) val recent = now.date == time.date || - (now.date.minus(time.date).days == 1 && now.hour < 12 && time.hour >= 18 ) + (period.years == 0 && period.months == 0 && period.days == 1 && now.hour < 12 && time.hour >= 18 ) val dateFormatter = if (recent) { DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 4b49f7ecf..1fe081d0d 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2283,10 +2283,22 @@ let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute() let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits) public func formatTimestampText(_ date: Date) -> Text { - let now = Calendar.current.dateComponents([.day, .hour], from: .now) - let dc = Calendar.current.dateComponents([.day, .hour], from: date) - let recent = now.day == dc.day || ((now.day ?? 0) - (dc.day ?? 0) == 1 && (dc.hour ?? 0) >= 18 && (now.hour ?? 0) < 12) - return Text(date, format: recent ? msgTimeFormat : msgDateFormat) + return Text(date, format: recent(date) ? msgTimeFormat : msgDateFormat) +} + +private func recent(_ date: Date) -> Bool { + let now = Date() + let calendar = Calendar.current + + guard let previousDay = calendar.date(byAdding: DateComponents(day: -1), to: now), + let previousDay18 = calendar.date(bySettingHour: 18, minute: 0, second: 0, of: previousDay), + let currentDay00 = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: now), + let currentDay12 = calendar.date(bySettingHour: 12, minute: 0, second: 0, of: now) else { + return false + } + + let isSameDay = calendar.isDate(date, inSameDayAs: now) + return isSameDay || (now < currentDay12 && date >= previousDay18 && date < currentDay00) } public enum CIStatus: Decodable { diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 1f224345c..c101f69fd 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -374,6 +374,7 @@ test-suite simplex-chat-test MobileTests ProtocolTests SchemaDump + ViewTests WebRTCTests Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 92a08091b..61d4c72fb 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -24,9 +24,10 @@ import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) -import Data.Time.Clock (DiffTime, UTCTime) +import Data.Time (LocalTime (..), TimeOfDay (..), TimeZone (..), utcToLocalTime) +import Data.Time.Calendar (addDays) +import Data.Time.Clock (UTCTime) import Data.Time.Format (defaultTimeLocale, formatTime) -import Data.Time.LocalTime (TimeZone, localDay, localTimeOfDay, timeOfDayToTime, utcToLocalTime) import Data.Word (Word32) import GHC.Generics (Generic) import qualified Network.HTTP.Types as Q @@ -1107,16 +1108,22 @@ receivedWithTime_ ts tz from quote CIMeta {itemId, itemTs, itemEdited, itemDelet _ -> "" ttyMsgTime :: CurrentTime -> TimeZone -> UTCTime -> StyledString -ttyMsgTime currentTime tz time = - let localTime = utcToLocalTime tz time - localCurrentTime = utcToLocalTime tz currentTime - fmt = - if (localDay localTime < localDay localCurrentTime) - && (timeOfDayToTime (localTimeOfDay localTime) > (6 * 60 * 60 :: DiffTime)) - then "%m-%d" -- if message is from yesterday or before and 6 hours has passed since midnight - else "%H:%M" +ttyMsgTime now tz time = + let fmt = if recent now tz time then "%H:%M" else "%m-%d" + localTime = utcToLocalTime tz time in styleTime $ formatTime defaultTimeLocale fmt localTime +recent :: CurrentTime -> TimeZone -> UTCTime -> Bool +recent now tz time = do + let localNow = utcToLocalTime tz now + localNowDay = localDay localNow + localTime = utcToLocalTime tz time + localTimeDay = localDay localTime + previousDay18 = LocalTime (addDays (-1) localNowDay) (TimeOfDay 18 0 0) + currentDay12 = LocalTime localNowDay (TimeOfDay 12 0 0) + localNowDay == localTimeDay + || (localNow < currentDay12 && localTime >= previousDay18 && localTimeDay < localNowDay) + viewSentMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] viewSentMessage to quote mc ts tz meta@CIMeta {itemEdited, itemDeleted, itemLive} = sentWithTime_ ts tz (prependFirst to $ quote <> prependFirst (indent <> live) (ttyMsgContent mc)) meta where diff --git a/tests/Test.hs b/tests/Test.hs index 4ea3e9ef5..9010aefa0 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -8,6 +8,7 @@ import ProtocolTests import SchemaDump import Test.Hspec import UnliftIO.Temporary (withTempDirectory) +import ViewTests import WebRTCTests main :: IO () @@ -15,6 +16,7 @@ main = do setLogLevel LogError -- LogDebug withGlobalLogging logCfg . hspec $ do describe "SimpleX chat markdown" markdownTests + describe "SimpleX chat view" viewTests describe "SimpleX chat protocol" protocolTests describe "WebRTC encryption" webRTCTests describe "Schema dump" schemaDumpTest diff --git a/tests/ViewTests.hs b/tests/ViewTests.hs new file mode 100644 index 000000000..7c7a2f0e0 --- /dev/null +++ b/tests/ViewTests.hs @@ -0,0 +1,86 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE OverloadedStrings #-} + +module ViewTests where + +import Data.Time +import Simplex.Chat.View +import Test.Hspec + +viewTests :: Spec +viewTests = do + testRecent + +testRecent :: Spec +testRecent = describe "recent" $ do + let tz = hoursToTimeZone 1 + now1159 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 10 * 3600 + 59 * 60) -- 11:59 in tz + now1200 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 11 * 3600) -- 12:00 in tz + today0000 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 23 * 3600) -- 00:00 in tz + today0600 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 5 * 3600) -- 06:00 in tz + today1200 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 11 * 3600) -- 12:00 in tz + today1800 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 17 * 3600) -- 18:00 in tz + today2359 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 22 * 3600 + 59 * 60) -- 23:59 in tz + yesterday0000 = UTCTime (fromGregorian 2023 6 5) (secondsToDiffTime $ 23 * 3600) -- 00:00 in tz + yesterday1759 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 16 * 3600 + 59 * 60) -- 17:59 in tz + yesterday1800 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 17 * 3600) -- 18:00 in tz + yesterday2359 = UTCTime (fromGregorian 2023 6 6) (secondsToDiffTime $ 22 * 3600 + 59 * 60) -- 23:59 in tz + sameDayLastMonth1900 = UTCTime (fromGregorian 2023 5 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz + prevDayLastMonth1900 = UTCTime (fromGregorian 2023 5 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz + sameDayLastYear1900 = UTCTime (fromGregorian 2022 6 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz + prevDayLastYear1900 = UTCTime (fromGregorian 2022 6 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz + tomorrow0000 = UTCTime (fromGregorian 2023 6 7) (secondsToDiffTime $ 23 * 3600) -- 00:00 in tz + tomorrow1759 = UTCTime (fromGregorian 2023 6 8) (secondsToDiffTime $ 16 * 3600 + 59 * 60) -- 17:59 in tz + tomorrow1800 = UTCTime (fromGregorian 2023 6 8) (secondsToDiffTime $ 17 * 3600) -- 18:00 in tz + tomorrow2359 = UTCTime (fromGregorian 2023 6 8) (secondsToDiffTime $ 22 * 3600 + 59 * 60) -- 23:59 in tz + sameDayNextMonth1900 = UTCTime (fromGregorian 2023 7 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz + prevDayNextMonth1900 = UTCTime (fromGregorian 2023 7 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz + sameDayNextYear1900 = UTCTime (fromGregorian 2024 6 7) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz + prevDayNextYear1900 = UTCTime (fromGregorian 2024 6 6) (secondsToDiffTime $ 18 * 3600) -- 19:00 in tz + test tz now1159 today0000 True + test tz now1159 today0600 True + test tz now1159 today1200 True + test tz now1159 today1800 True + test tz now1159 today2359 True + test tz now1159 yesterday0000 False + test tz now1159 yesterday1759 False + test tz now1159 yesterday1800 True + test tz now1159 yesterday2359 True + test tz now1159 sameDayLastMonth1900 False + test tz now1159 prevDayLastMonth1900 False + test tz now1159 sameDayLastYear1900 False + test tz now1159 prevDayLastYear1900 False + test tz now1159 tomorrow0000 False + test tz now1159 tomorrow1759 False + test tz now1159 tomorrow1800 False + test tz now1159 tomorrow2359 False + test tz now1159 sameDayNextMonth1900 False + test tz now1159 prevDayNextMonth1900 False + test tz now1159 sameDayNextYear1900 False + test tz now1159 prevDayNextYear1900 False + + test tz now1200 today0000 True + test tz now1200 today0600 True + test tz now1200 today1200 True + test tz now1200 today1800 True + test tz now1200 today2359 True + test tz now1200 yesterday0000 False + test tz now1200 yesterday1759 False + test tz now1200 yesterday1800 False + test tz now1200 yesterday2359 False + test tz now1200 sameDayLastMonth1900 False + test tz now1200 prevDayLastMonth1900 False + test tz now1200 sameDayLastYear1900 False + test tz now1200 prevDayLastYear1900 False + test tz now1200 tomorrow0000 False + test tz now1200 tomorrow1759 False + test tz now1200 tomorrow1800 False + test tz now1200 tomorrow2359 False + test tz now1200 sameDayNextMonth1900 False + test tz now1200 prevDayNextMonth1900 False + test tz now1200 sameDayNextYear1900 False + test tz now1200 prevDayNextYear1900 False + where + test tz now time expected = + it ("returns " <> show expected <> " for time " <> show time <> " when time zone is " <> show tz <> " and current time is " <> show now) $ + recent now tz time `shouldBe` expected