From 611ff0c7e4e0233dc69b4bd0f8d2bb59de685ace Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Tue, 30 Apr 2024 19:31:30 +0100
Subject: [PATCH] [Clipclops] Add clop sent time to clipclop (#3772)

* add message sent time to message

* fix last message in group logic
---
 .../Messages/Conversation/MessageItem.tsx     | 149 +++++++++++++++---
 .../Messages/Conversation/MessagesList.tsx    |  48 +++---
 src/screens/Messages/Temp/query/query.ts      |   8 +-
 src/view/com/util/TimeElapsed.tsx             |   8 +-
 4 files changed, 162 insertions(+), 51 deletions(-)

diff --git a/src/screens/Messages/Conversation/MessageItem.tsx b/src/screens/Messages/Conversation/MessageItem.tsx
index 822b1780..688c4244 100644
--- a/src/screens/Messages/Conversation/MessageItem.tsx
+++ b/src/screens/Messages/Conversation/MessageItem.tsx
@@ -1,37 +1,140 @@
-import React from 'react'
-import {View} from 'react-native'
+import React, {useCallback} from 'react'
+import {StyleProp, TextStyle, View} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 import {useAgent} from '#/state/session'
+import {TimeElapsed} from '#/view/com/util/TimeElapsed'
 import {atoms as a, useTheme} from '#/alf'
 import {Text} from '#/components/Typography'
 import * as TempDmChatDefs from '#/temp/dm/defs'
 
-export function MessageItem({item}: {item: TempDmChatDefs.MessageView}) {
+export function MessageItem({
+  item,
+  next,
+}: {
+  item: TempDmChatDefs.MessageView
+  next: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage | null
+}) {
   const t = useTheme()
   const {getAgent} = useAgent()
 
-  const fromMe = item.sender?.did === getAgent().session?.did
+  const isFromSelf = item.sender?.did === getAgent().session?.did
+
+  const isNextFromSelf =
+    TempDmChatDefs.isMessageView(next) &&
+    next.sender?.did === getAgent().session?.did
+
+  const isLastInGroup = !next || isFromSelf ? !isNextFromSelf : isNextFromSelf
 
   return (
-    <View
-      style={[
-        a.py_sm,
-        a.px_md,
-        a.my_xs,
-        a.rounded_md,
-        fromMe ? a.self_end : a.self_start,
-        {
-          backgroundColor: fromMe
-            ? t.palette.primary_500
-            : t.palette.contrast_50,
-          maxWidth: '65%',
-          borderRadius: 17,
-        },
-      ]}>
-      <Text
-        style={[a.text_md, a.leading_snug, fromMe && {color: t.palette.white}]}>
-        {item.text}
-      </Text>
+    <View>
+      <View
+        style={[
+          a.py_sm,
+          a.px_lg,
+          a.my_2xs,
+          a.rounded_md,
+          isFromSelf ? a.self_end : a.self_start,
+          {
+            maxWidth: '65%',
+            backgroundColor: isFromSelf
+              ? t.palette.primary_500
+              : t.palette.contrast_50,
+            borderRadius: 17,
+          },
+          isFromSelf
+            ? {borderBottomRightRadius: isLastInGroup ? 2 : 17}
+            : {borderBottomLeftRadius: isLastInGroup ? 2 : 17},
+        ]}>
+        <Text
+          style={[
+            a.text_md,
+            a.leading_snug,
+            isFromSelf && {color: t.palette.white},
+          ]}>
+          {item.text}
+        </Text>
+      </View>
+      <Metadata
+        message={item}
+        isLastInGroup={isLastInGroup}
+        style={isFromSelf ? a.text_right : a.text_left}
+      />
     </View>
   )
 }
+
+function Metadata({
+  message,
+  isLastInGroup,
+  style,
+}: {
+  message: TempDmChatDefs.MessageView
+  isLastInGroup: boolean
+  style: StyleProp<TextStyle>
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+
+  const relativeTimestamp = useCallback(
+    (timestamp: string) => {
+      const date = new Date(timestamp)
+      const now = new Date()
+
+      const time = new Intl.DateTimeFormat(undefined, {
+        hour: 'numeric',
+        minute: 'numeric',
+        hour12: true,
+      }).format(date)
+
+      const diff = now.getTime() - date.getTime()
+
+      // under 1 minute
+      if (diff < 1000 * 60) {
+        return _(msg`Now`)
+      }
+
+      // in the last day
+      if (now.getDate() === date.getDate()) {
+        return time
+      }
+
+      // if yesterday
+      if (diff < 24 * 60 * 60 * 1000 && now.getDate() - date.getDate() === 1) {
+        return _(msg`Yesterday, ${time}`)
+      }
+
+      return new Intl.DateTimeFormat(undefined, {
+        hour: 'numeric',
+        minute: 'numeric',
+        hour12: true,
+        day: 'numeric',
+        month: 'numeric',
+        year: 'numeric',
+      }).format(date)
+    },
+    [_],
+  )
+
+  if (!isLastInGroup) {
+    return null
+  }
+
+  return (
+    <TimeElapsed timestamp={message.sentAt} timeToString={relativeTimestamp}>
+      {({timeElapsed}) => (
+        <Text
+          style={[
+            t.atoms.text_contrast_medium,
+            a.text_xs,
+            a.mt_xs,
+            a.mb_lg,
+            style,
+          ]}>
+          {timeElapsed}
+        </Text>
+      )}
+    </TimeElapsed>
+  )
+}
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
index e3b518f6..fe353fa3 100644
--- a/src/screens/Messages/Conversation/MessagesList.tsx
+++ b/src/screens/Messages/Conversation/MessagesList.tsx
@@ -3,7 +3,7 @@ import {FlatList, View, ViewToken} from 'react-native'
 import {Alert} from 'react-native'
 import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
 
-import {isWeb} from 'platform/detection'
+import {isWeb} from '#/platform/detection'
 import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
 import {MessageItem} from '#/screens/Messages/Conversation/MessageItem'
 import {
@@ -15,6 +15,11 @@ import {Loader} from '#/components/Loader'
 import {Text} from '#/components/Typography'
 import * as TempDmChatDefs from '#/temp/dm/defs'
 
+type MessageWithNext = {
+  message: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage
+  next: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage | null
+}
+
 function MaybeLoader({isLoading}: {isLoading: boolean}) {
   return (
     <View
@@ -29,12 +34,9 @@ function MaybeLoader({isLoading}: {isLoading: boolean}) {
   )
 }
 
-function renderItem({
-  item,
-}: {
-  item: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage
-}) {
-  if (TempDmChatDefs.isMessageView(item)) return <MessageItem item={item} />
+function renderItem({item}: {item: MessageWithNext}) {
+  if (TempDmChatDefs.isMessageView(item.message))
+    return <MessageItem item={item.message} next={item.next} />
 
   if (TempDmChatDefs.isDeletedMessage(item)) return <Text>Deleted message</Text>
 
@@ -136,18 +138,24 @@ export function MessagesList({chatId}: {chatId: string}) {
   const messages = useMemo(() => {
     if (!chat) return []
 
-    const filtered = chat.messages.filter(
-      (
-        message,
-      ): message is
-        | TempDmChatDefs.MessageView
-        | TempDmChatDefs.DeletedMessage => {
-        return (
-          TempDmChatDefs.isMessageView(message) ||
-          TempDmChatDefs.isDeletedMessage(message)
-        )
-      },
-    )
+    const filtered = chat.messages
+      .filter(
+        (
+          message,
+        ): message is
+          | TempDmChatDefs.MessageView
+          | TempDmChatDefs.DeletedMessage => {
+          return (
+            TempDmChatDefs.isMessageView(message) ||
+            TempDmChatDefs.isDeletedMessage(message)
+          )
+        },
+      )
+      .reduce((acc, message) => {
+        // convert [n1, n2, n3, ...] to [{message: n1, next: n2}, {message: n2, next: n3}, {message: n3, next: n4}, ...]
+
+        return [...acc, {message, next: acc.at(-1)?.message ?? null}]
+      }, [] as MessageWithNext[])
     totalMessages.current = filtered.length
 
     return filtered
@@ -161,7 +169,7 @@ export function MessagesList({chatId}: {chatId: string}) {
       contentContainerStyle={{flex: 1}}>
       <FlatList
         data={messages}
-        keyExtractor={item => item.id}
+        keyExtractor={item => item.message.id}
         renderItem={renderItem}
         contentContainerStyle={{paddingHorizontal: 10}}
         // In the future, we might want to adjust this value. Not very concerning right now as long as we are only
diff --git a/src/screens/Messages/Temp/query/query.ts b/src/screens/Messages/Temp/query/query.ts
index d207f04a..a51929bc 100644
--- a/src/screens/Messages/Temp/query/query.ts
+++ b/src/screens/Messages/Temp/query/query.ts
@@ -140,6 +140,7 @@ export function useSendMessageMutation(chatId: string) {
               id: variables.tempId,
               text: variables.message,
               sender: {did: headers.Authorization}, // TODO a real DID get
+              sentAt: new Date().toISOString(),
             },
             ...prev.messages,
           ],
@@ -151,12 +152,7 @@ export function useSendMessageMutation(chatId: string) {
         return {
           ...prev,
           messages: prev.messages.map(m =>
-            m.id === variables.tempId
-              ? {
-                  ...m,
-                  id: result.id,
-                }
-              : m,
+            m.id === variables.tempId ? {...m, id: result.id} : m,
           ),
         }
       })
diff --git a/src/view/com/util/TimeElapsed.tsx b/src/view/com/util/TimeElapsed.tsx
index 02b0f231..a5d3a537 100644
--- a/src/view/com/util/TimeElapsed.tsx
+++ b/src/view/com/util/TimeElapsed.tsx
@@ -6,17 +6,21 @@ import {ago} from 'lib/strings/time'
 export function TimeElapsed({
   timestamp,
   children,
+  timeToString = ago,
 }: {
   timestamp: string
   children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
+  timeToString?: (timeElapsed: string) => string
 }) {
   const tick = useTickEveryMinute()
-  const [timeElapsed, setTimeAgo] = React.useState(() => ago(timestamp))
+  const [timeElapsed, setTimeAgo] = React.useState(() =>
+    timeToString(timestamp),
+  )
 
   const [prevTick, setPrevTick] = React.useState(tick)
   if (prevTick !== tick) {
     setPrevTick(tick)
-    setTimeAgo(ago(timestamp))
+    setTimeAgo(timeToString(timestamp))
   }
 
   return children({timeElapsed})