about summary refs log tree commit diff
path: root/src/components/dms/MessageItem.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/dms/MessageItem.tsx')
-rw-r--r--src/components/dms/MessageItem.tsx198
1 files changed, 198 insertions, 0 deletions
diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx
new file mode 100644
index 000000000..f8f5197ca
--- /dev/null
+++ b/src/components/dms/MessageItem.tsx
@@ -0,0 +1,198 @@
+import React, {useCallback, useMemo, useRef} from 'react'
+import {LayoutAnimation, StyleProp, TextStyle, View} from 'react-native'
+import {ChatBskyConvoDefs} from '@atproto-labs/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useSession} from '#/state/session'
+import {TimeElapsed} from 'view/com/util/TimeElapsed'
+import {atoms as a, useTheme} from '#/alf'
+import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
+import {Text} from '#/components/Typography'
+
+export let MessageItem = ({
+  item,
+  next,
+  pending,
+}: {
+  item: ChatBskyConvoDefs.MessageView
+  next:
+    | ChatBskyConvoDefs.MessageView
+    | ChatBskyConvoDefs.DeletedMessageView
+    | null
+  pending?: boolean
+}): React.ReactNode => {
+  const t = useTheme()
+  const {currentAccount} = useSession()
+
+  const isFromSelf = item.sender?.did === currentAccount?.did
+
+  const isNextFromSelf =
+    ChatBskyConvoDefs.isMessageView(next) &&
+    next.sender?.did === currentAccount?.did
+
+  const isLastInGroup = useMemo(() => {
+    // TODO this means it's a placeholder. Let's figure out the right way to do this though!
+    if (item.id.length > 13) {
+      return false
+    }
+
+    // if the next message is from a different sender, then it's the last in the group
+    if (isFromSelf ? !isNextFromSelf : isNextFromSelf) {
+      return true
+    }
+
+    // or, if there's a 3 minute gap between this message and the next
+    if (ChatBskyConvoDefs.isMessageView(next)) {
+      const thisDate = new Date(item.sentAt)
+      const nextDate = new Date(next.sentAt)
+
+      const diff = nextDate.getTime() - thisDate.getTime()
+
+      // 3 minutes
+      return diff > 3 * 60 * 1000
+    }
+
+    return true
+  }, [item, next, isFromSelf, isNextFromSelf])
+
+  const lastInGroupRef = useRef(isLastInGroup)
+  if (lastInGroupRef.current !== isLastInGroup) {
+    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+    lastInGroupRef.current = isLastInGroup
+  }
+
+  const pendingColor =
+    t.name === 'light' ? t.palette.primary_200 : t.palette.primary_800
+
+  return (
+    <View>
+      <ActionsWrapper isFromSelf={isFromSelf} message={item}>
+        <View
+          style={[
+            a.py_sm,
+            a.my_2xs,
+            a.rounded_md,
+            {
+              paddingLeft: 14,
+              paddingRight: 14,
+              backgroundColor: isFromSelf
+                ? pending
+                  ? pendingColor
+                  : 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},
+              pending && t.name !== 'light' && {color: t.palette.primary_300},
+            ]}>
+            {item.text}
+          </Text>
+        </View>
+      </ActionsWrapper>
+      <MessageItemMetadata
+        message={item}
+        isLastInGroup={isLastInGroup}
+        style={isFromSelf ? a.text_right : a.text_left}
+      />
+    </View>
+  )
+}
+
+MessageItem = React.memo(MessageItem)
+
+let MessageItemMetadata = ({
+  message,
+  isLastInGroup,
+  style,
+}: {
+  message: ChatBskyConvoDefs.MessageView
+  isLastInGroup: boolean
+  style: StyleProp<TextStyle>
+}): React.ReactNode => {
+  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()
+
+      // if under 1 minute
+      if (diff < 1000 * 60) {
+        return _(msg`Now`)
+      }
+
+      // if in the last day
+      if (localDateString(now) === localDateString(date)) {
+        return time
+      }
+
+      // if yesterday
+      const yesterday = new Date(now)
+      yesterday.setDate(yesterday.getDate() - 1)
+
+      if (localDateString(yesterday) === localDateString(date)) {
+        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_2xs,
+            a.mb_lg,
+            style,
+          ]}>
+          {timeElapsed}
+        </Text>
+      )}
+    </TimeElapsed>
+  )
+}
+
+MessageItemMetadata = React.memo(MessageItemMetadata)
+
+function localDateString(date: Date) {
+  // can't use toISOString because it should be in local time
+  const mm = date.getMonth()
+  const dd = date.getDate()
+  const yyyy = date.getFullYear()
+  // not padding with 0s because it's not necessary, it's just used for comparison
+  return `${yyyy}-${mm}-${dd}`
+}