@planningcenter/chat-react-native 3.16.0-rc.2 → 3.16.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/build/components/conversation/attachments/giphy_attachment.d.ts.map +1 -1
  2. package/build/components/conversation/attachments/giphy_attachment.js +1 -11
  3. package/build/components/conversation/attachments/giphy_attachment.js.map +1 -1
  4. package/build/components/conversation/message.d.ts.map +1 -1
  5. package/build/components/conversation/message.js +5 -21
  6. package/build/components/conversation/message.js.map +1 -1
  7. package/build/components/conversation/shadow_message.d.ts +7 -0
  8. package/build/components/conversation/shadow_message.d.ts.map +1 -0
  9. package/build/components/conversation/shadow_message.js +156 -0
  10. package/build/components/conversation/shadow_message.js.map +1 -0
  11. package/build/components/primitive/avatar_primitive.d.ts +1 -0
  12. package/build/components/primitive/avatar_primitive.d.ts.map +1 -1
  13. package/build/components/primitive/avatar_primitive.js +4 -0
  14. package/build/components/primitive/avatar_primitive.js.map +1 -1
  15. package/build/hooks/index.d.ts +1 -0
  16. package/build/hooks/index.d.ts.map +1 -1
  17. package/build/hooks/index.js +1 -0
  18. package/build/hooks/index.js.map +1 -1
  19. package/build/hooks/use_animated_message_background_color.d.ts +8 -0
  20. package/build/hooks/use_animated_message_background_color.d.ts.map +1 -0
  21. package/build/hooks/use_animated_message_background_color.js +29 -0
  22. package/build/hooks/use_animated_message_background_color.js.map +1 -0
  23. package/build/utils/assert_keys_are_numbers.d.ts +2 -0
  24. package/build/utils/assert_keys_are_numbers.d.ts.map +1 -0
  25. package/build/utils/assert_keys_are_numbers.js +12 -0
  26. package/build/utils/assert_keys_are_numbers.js.map +1 -0
  27. package/build/utils/index.d.ts +1 -0
  28. package/build/utils/index.d.ts.map +1 -1
  29. package/build/utils/index.js +1 -0
  30. package/build/utils/index.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/components/conversation/attachments/giphy_attachment.tsx +1 -14
  33. package/src/components/conversation/message.tsx +11 -35
  34. package/src/components/conversation/shadow_message.tsx +266 -0
  35. package/src/components/primitive/avatar_primitive.tsx +4 -0
  36. package/src/hooks/index.ts +1 -0
  37. package/src/hooks/use_animated_message_background_color.ts +44 -0
  38. package/src/utils/assert_keys_are_numbers.ts +13 -0
  39. package/src/utils/index.ts +1 -0
@@ -1,4 +1,5 @@
1
1
  export * from './use_async_storage';
2
+ export * from './use_animated_message_background_color';
2
3
  export * from './use_theme';
3
4
  export * from './use_suspense_api';
4
5
  export * from './use_current_person';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA;AAClC,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,gCAAgC,CAAA","sourcesContent":["export * from './use_async_storage'\nexport * from './use_theme'\nexport * from './use_suspense_api'\nexport * from './use_current_person'\nexport * from './use_font_scale'\nexport * from './use_create_android_ripple_color'\nexport * from './use_chat_permissions'\nexport * from './use_api_client'\nexport * from './use_groups_groups'\nexport * from './use_groups'\nexport * from './use_api'\nexport * from './use_api_client'\nexport * from './use_message_reaction_toggle'\nexport * from './use_interaction_ghost_color'\nexport * from './use_at_font_scale_breakpoint'\nexport * from './use_scalable_number_of_lines'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,yCAAyC,CAAA;AACvD,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA;AAClC,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,gCAAgC,CAAA","sourcesContent":["export * from './use_async_storage'\nexport * from './use_animated_message_background_color'\nexport * from './use_theme'\nexport * from './use_suspense_api'\nexport * from './use_current_person'\nexport * from './use_font_scale'\nexport * from './use_create_android_ripple_color'\nexport * from './use_chat_permissions'\nexport * from './use_api_client'\nexport * from './use_groups_groups'\nexport * from './use_groups'\nexport * from './use_api'\nexport * from './use_api_client'\nexport * from './use_message_reaction_toggle'\nexport * from './use_interaction_ghost_color'\nexport * from './use_at_font_scale_breakpoint'\nexport * from './use_scalable_number_of_lines'\n"]}
@@ -0,0 +1,8 @@
1
+ export declare function useAnimatedMessageBackgroundColor(): {
2
+ animatedBackgroundColor: {
3
+ backgroundColor: string;
4
+ };
5
+ handleMessagePressIn: () => void;
6
+ handleMessagePressOut: () => void;
7
+ };
8
+ //# sourceMappingURL=use_animated_message_background_color.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use_animated_message_background_color.d.ts","sourceRoot":"","sources":["../../src/hooks/use_animated_message_background_color.ts"],"names":[],"mappings":"AAUA,wBAAgB,iCAAiC;;;;;;EAiChD"}
@@ -0,0 +1,29 @@
1
+ import { Platform } from 'react-native';
2
+ import { useSharedValue, useAnimatedStyle, withTiming, interpolateColor, Easing, } from 'react-native-reanimated';
3
+ import { useTheme } from '../hooks';
4
+ export function useAnimatedMessageBackgroundColor() {
5
+ const { colors } = useTheme();
6
+ const bgFadeProgress = useSharedValue(0);
7
+ const pressedColor = Platform.select({
8
+ ios: colors.fillColorNeutral050Base,
9
+ default: 'transparent',
10
+ });
11
+ const animatedBackgroundColor = useAnimatedStyle(() => {
12
+ const backgroundColor = interpolateColor(bgFadeProgress.value, [0, 1], ['transparent', pressedColor]);
13
+ return {
14
+ backgroundColor,
15
+ };
16
+ });
17
+ const handleMessagePressIn = () => {
18
+ bgFadeProgress.value = withTiming(1, { duration: 300, easing: Easing.inOut(Easing.ease) });
19
+ };
20
+ const handleMessagePressOut = () => {
21
+ bgFadeProgress.value = withTiming(0, { duration: 300, easing: Easing.inOut(Easing.ease) });
22
+ };
23
+ return {
24
+ animatedBackgroundColor,
25
+ handleMessagePressIn,
26
+ handleMessagePressOut,
27
+ };
28
+ }
29
+ //# sourceMappingURL=use_animated_message_background_color.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use_animated_message_background_color.js","sourceRoot":"","sources":["../../src/hooks/use_animated_message_background_color.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,gBAAgB,EAChB,MAAM,GACP,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAEnC,MAAM,UAAU,iCAAiC;IAC/C,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAA;IAC7B,MAAM,cAAc,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA;IAExC,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC;QACnC,GAAG,EAAE,MAAM,CAAC,uBAAuB;QACnC,OAAO,EAAE,aAAa;KACvB,CAAC,CAAA;IAEF,MAAM,uBAAuB,GAAG,gBAAgB,CAAC,GAAG,EAAE;QACpD,MAAM,eAAe,GAAG,gBAAgB,CACtC,cAAc,CAAC,KAAK,EACpB,CAAC,CAAC,EAAE,CAAC,CAAC,EACN,CAAC,aAAa,EAAE,YAAY,CAAC,CAC9B,CAAA;QACD,OAAO;YACL,eAAe;SAChB,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,MAAM,oBAAoB,GAAG,GAAG,EAAE;QAChC,cAAc,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC5F,CAAC,CAAA;IAED,MAAM,qBAAqB,GAAG,GAAG,EAAE;QACjC,cAAc,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC5F,CAAC,CAAA;IAED,OAAO;QACL,uBAAuB;QACvB,oBAAoB;QACpB,qBAAqB;KACtB,CAAA;AACH,CAAC","sourcesContent":["import { Platform } from 'react-native'\nimport {\n useSharedValue,\n useAnimatedStyle,\n withTiming,\n interpolateColor,\n Easing,\n} from 'react-native-reanimated'\nimport { useTheme } from '../hooks'\n\nexport function useAnimatedMessageBackgroundColor() {\n const { colors } = useTheme()\n const bgFadeProgress = useSharedValue(0)\n\n const pressedColor = Platform.select({\n ios: colors.fillColorNeutral050Base,\n default: 'transparent',\n })\n\n const animatedBackgroundColor = useAnimatedStyle(() => {\n const backgroundColor = interpolateColor(\n bgFadeProgress.value,\n [0, 1],\n ['transparent', pressedColor]\n )\n return {\n backgroundColor,\n }\n })\n\n const handleMessagePressIn = () => {\n bgFadeProgress.value = withTiming(1, { duration: 300, easing: Easing.inOut(Easing.ease) })\n }\n\n const handleMessagePressOut = () => {\n bgFadeProgress.value = withTiming(0, { duration: 300, easing: Easing.inOut(Easing.ease) })\n }\n\n return {\n animatedBackgroundColor,\n handleMessagePressIn,\n handleMessagePressOut,\n }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export declare const assertKeysAreNumbers: (obj: Object) => Record<any, number>;
2
+ //# sourceMappingURL=assert_keys_are_numbers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert_keys_are_numbers.d.ts","sourceRoot":"","sources":["../../src/utils/assert_keys_are_numbers.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,QAAS,MAAM,KAAG,MAAM,CAAC,GAAG,EAAE,MAAM,CAEpE,CAAA"}
@@ -0,0 +1,12 @@
1
+ export const assertKeysAreNumbers = (obj) => {
2
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, assertNumber(value)]));
3
+ };
4
+ const assertNumber = (value) => {
5
+ if (typeof value === 'number')
6
+ return value;
7
+ if (typeof value === 'string' && !isNaN(Number(value))) {
8
+ return Number(value);
9
+ }
10
+ return 0;
11
+ };
12
+ //# sourceMappingURL=assert_keys_are_numbers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert_keys_are_numbers.js","sourceRoot":"","sources":["../../src/utils/assert_keys_are_numbers.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,GAAW,EAAuB,EAAE;IACvE,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AAClG,CAAC,CAAA;AAED,MAAM,YAAY,GAAG,CAAC,KAAa,EAAU,EAAE;IAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAE3C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACvD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACtB,CAAC;IAED,OAAO,CAAC,CAAA;AACV,CAAC,CAAA","sourcesContent":["export const assertKeysAreNumbers = (obj: Object): Record<any, number> => {\n return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, assertNumber(value)]))\n}\n\nconst assertNumber = (value: string): number => {\n if (typeof value === 'number') return value\n\n if (typeof value === 'string' && !isNaN(Number(value))) {\n return Number(value)\n }\n\n return 0\n}\n"]}
@@ -8,4 +8,5 @@ export * from './native_adapters';
8
8
  export * from './pluralize';
9
9
  export * from './destructure_chat_group_graph_id';
10
10
  export * from './convert_attachments_for_create';
11
+ export * from './assert_keys_are_numbers';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA"}
@@ -8,4 +8,5 @@ export * from './native_adapters';
8
8
  export * from './pluralize';
9
9
  export * from './destructure_chat_group_graph_id';
10
10
  export * from './convert_attachments_for_create';
11
+ export * from './assert_keys_are_numbers';
11
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA","sourcesContent":["export * from './session'\nexport * from './theme'\nexport * from './styles'\nexport * from './client'\nexport * from './uri'\nexport * from './cache'\nexport * from './native_adapters'\nexport * from './pluralize'\nexport * from './destructure_chat_group_graph_id'\nexport * from './convert_attachments_for_create'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA","sourcesContent":["export * from './session'\nexport * from './theme'\nexport * from './styles'\nexport * from './client'\nexport * from './uri'\nexport * from './cache'\nexport * from './native_adapters'\nexport * from './pluralize'\nexport * from './destructure_chat_group_graph_id'\nexport * from './convert_attachments_for_create'\nexport * from './assert_keys_are_numbers'\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.16.0-rc.2",
3
+ "version": "3.16.0-rc.3",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -55,5 +55,5 @@
55
55
  "react-native-url-polyfill": "^2.0.0",
56
56
  "typescript": "<5.6.0"
57
57
  },
58
- "gitHead": "7049e04c5d4b3cc2b6d30b5869c66dba504244b1"
58
+ "gitHead": "41458eebf252d441367b1c30a6b35e5422486bf2"
59
59
  }
@@ -5,6 +5,7 @@ import { useTheme } from '../../../hooks'
5
5
  import { DenormalizedGiphyAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
6
6
  import { PlatformPressable } from '@react-navigation/elements'
7
7
  import { Image } from '../../display'
8
+ import { assertKeysAreNumbers } from '../../../utils'
8
9
 
9
10
  export function GiphyAttachment({
10
11
  attachment,
@@ -46,20 +47,6 @@ export function GiphyAttachment({
46
47
  )
47
48
  }
48
49
 
49
- const assertKeysAreNumbers = (obj: Object): Record<any, number> => {
50
- return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, assertNumber(value)]))
51
- }
52
-
53
- const assertNumber = (value: string): number => {
54
- if (typeof value === 'number') return value
55
-
56
- if (typeof value === 'string' && !isNaN(Number(value))) {
57
- return Number(value)
58
- }
59
-
60
- return 0
61
- }
62
-
63
50
  const useStyles = ({ imageWidth, imageHeight }: { imageWidth: number; imageHeight: number }) => {
64
51
  const { colors } = useTheme()
65
52
  return StyleSheet.create({
@@ -1,9 +1,13 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import React, { useEffect } from 'react'
3
- import { Platform, Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
3
+ import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
4
4
  import { MessageReaction } from '../../components/conversation/message_reaction'
5
5
  import { Avatar, Icon, Spinner, Text, TextInlineButton } from '../../components/display'
6
- import { useInteractionGhostBackgroundColor, useTheme } from '../../hooks'
6
+ import {
7
+ useAnimatedMessageBackgroundColor,
8
+ useInteractionGhostBackgroundColor,
9
+ useTheme,
10
+ } from '../../hooks'
7
11
  import { MessageResource } from '../../types'
8
12
  import { ReactionCountResource } from '../../types/resources/reaction'
9
13
  import { MessageAttachments } from './message_attachments'
@@ -15,13 +19,7 @@ import {
15
19
  MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
16
20
  platformFontWeightMedium,
17
21
  } from '../../utils/styles'
18
- import Animated, {
19
- useSharedValue,
20
- useAnimatedStyle,
21
- withTiming,
22
- interpolateColor,
23
- Easing,
24
- } from 'react-native-reanimated'
22
+ import Animated from 'react-native-reanimated'
25
23
  import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
26
24
  import { MessageReadReceipts } from './message_read_receipts'
27
25
  import { isNewMessage, useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
@@ -56,6 +54,8 @@ export function Message({
56
54
  const isPersisted = !isNewMessage(message)
57
55
  const [temporarilyHidePendingState, setTemporarilyHidePendingState] = React.useState(true)
58
56
  const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
57
+ const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
58
+ useAnimatedMessageBackgroundColor()
59
59
 
60
60
  useEffect(() => {
61
61
  if (pending) {
@@ -81,30 +81,6 @@ export function Message({
81
81
  }
82
82
  }
83
83
 
84
- const bgFadeProgress = useSharedValue(0)
85
- const pressedColor = Platform.select({
86
- ios: colors.fillColorNeutral050Base,
87
- default: 'transparent',
88
- })
89
- const animatedBackgroundColor = useAnimatedStyle(() => {
90
- const backgroundColor = interpolateColor(
91
- bgFadeProgress.value,
92
- [0, 1],
93
- ['transparent', pressedColor]
94
- )
95
- return {
96
- backgroundColor,
97
- }
98
- })
99
-
100
- const handlePressIn = () => {
101
- bgFadeProgress.value = withTiming(1, { duration: 300, easing: Easing.inOut(Easing.ease) })
102
- }
103
-
104
- const handlePressOut = () => {
105
- bgFadeProgress.value = withTiming(0, { duration: 300, easing: Easing.inOut(Easing.ease) })
106
- }
107
-
108
84
  const handleMessageLongPress = () => {
109
85
  if (!isPersisted) return
110
86
 
@@ -149,8 +125,8 @@ export function Message({
149
125
  <Pressable
150
126
  onLongPress={handleMessageLongPress}
151
127
  onPress={() => setShowMessageMetaToggle(!showMessageMetaToggle)}
152
- onPressIn={handlePressIn}
153
- onPressOut={handlePressOut}
128
+ onPressIn={handleMessagePressIn}
129
+ onPressOut={handleMessagePressOut}
154
130
  android_ripple={{ color: colors.androidRippleNeutral }}
155
131
  accessibilityHint="Long press to view message actions like reacting and copying."
156
132
  >
@@ -0,0 +1,266 @@
1
+ import React from 'react'
2
+ import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
3
+ import { Avatar, Icon, IconProps, Image, Text } from '../display'
4
+ import {
5
+ useAnimatedMessageBackgroundColor,
6
+ useFontScale,
7
+ useScalableNumberOfLines,
8
+ useTheme,
9
+ } from '../../hooks'
10
+ import { MessageResource } from '../../types'
11
+ import {
12
+ DenormalizedAttachmentResource,
13
+ DenormalizedGiphyAttachmentResource,
14
+ DenormalizedExpandedLinkAttachmentResource,
15
+ DenormalizedMessageAttachmentResource,
16
+ } from '../../types/resources/denormalized_attachment_resource'
17
+ import {
18
+ CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
19
+ MAX_FONT_SIZE_MULTIPLIER,
20
+ MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
21
+ platformFontWeightMedium,
22
+ } from '../../utils/styles'
23
+ import Animated from 'react-native-reanimated'
24
+ import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
25
+ import { assertKeysAreNumbers } from '../../utils'
26
+
27
+ interface ShadowMessageProps extends MessageResource {}
28
+
29
+ export function ShadowMessage({ ...message }: ShadowMessageProps) {
30
+ const { text } = message
31
+ const styles = useStyles(message)
32
+ const { colors } = useTheme()
33
+ const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
34
+ const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
35
+ useAnimatedMessageBackgroundColor()
36
+ const scalableNumberOfLines = useScalableNumberOfLines(2)
37
+
38
+ const replyCount = 0 // TODO: Get reply count from message object
39
+
40
+ const handleNavigateToReplies = () => {
41
+ console.log('Navigate to replies') // TODO: Implement navigate to a reply screen
42
+ }
43
+
44
+ return (
45
+ <Pressable
46
+ android_ripple={{ color: colors.androidRippleNeutral }}
47
+ onPress={handleNavigateToReplies}
48
+ onPressIn={handleMessagePressIn}
49
+ onPressOut={handleMessagePressOut}
50
+ accessibilityHint="Navigate to all replies for this message."
51
+ >
52
+ <Animated.View style={[styles.message, animatedBackgroundColor]}>
53
+ {!message.mine && (
54
+ <View>
55
+ <View style={styles.avatarWrapper}>
56
+ <Avatar
57
+ size="xs"
58
+ sourceUri={message.author.avatar}
59
+ style={styles.avatar}
60
+ maxFontSizeMultiplier={1}
61
+ />
62
+ </View>
63
+ <TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
64
+ </View>
65
+ )}
66
+ <View style={styles.messageContent}>
67
+ <View
68
+ style={styles.messageBubble}
69
+ onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
70
+ >
71
+ <MessageAttachmentImagery attachments={message.attachments} />
72
+ {text && (
73
+ <Text
74
+ variant="footnote"
75
+ style={styles.messageText}
76
+ numberOfLines={scalableNumberOfLines}
77
+ >
78
+ {text}
79
+ </Text>
80
+ )}
81
+ </View>
82
+ <View style={styles.messageMeta}>
83
+ <Text variant="footnote" style={styles.replyCountText}>
84
+ {replyCount} replies
85
+ </Text>
86
+ </View>
87
+ </View>
88
+ {message.mine && (
89
+ <MyReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
90
+ )}
91
+ </Animated.View>
92
+ </Pressable>
93
+ )
94
+ }
95
+
96
+ function MessageAttachmentImagery({
97
+ attachments,
98
+ }: {
99
+ attachments: DenormalizedAttachmentResource[]
100
+ }) {
101
+ if (!attachments || attachments.length === 0) return null
102
+
103
+ const attachment = attachments[0]
104
+
105
+ if (attachment.type === 'giphy') {
106
+ return <GiphyImage attachment={attachment} />
107
+ }
108
+ if (attachment.type === 'ExpandedLink') {
109
+ return <ExpandedLinkImage attachment={attachment} />
110
+ }
111
+ if (attachment.type === 'MessageAttachment') {
112
+ const contentType = attachment.attributes?.contentType
113
+ const basicType = contentType?.split('/')[0]
114
+
115
+ switch (basicType) {
116
+ case 'image':
117
+ return <MessageAttachmentImage attachment={attachment} />
118
+ case 'video':
119
+ return <MessageAttachmentIcon iconName="general.outlinedVideoFile" />
120
+ case 'audio':
121
+ return <MessageAttachmentIcon iconName="general.outlinedMusicFile" />
122
+ case 'application':
123
+ return <MessageAttachmentIcon iconName="general.outlinedGenericFile" />
124
+ default:
125
+ return null
126
+ }
127
+ }
128
+
129
+ return null
130
+ }
131
+
132
+ function GiphyImage({ attachment }: { attachment: DenormalizedGiphyAttachmentResource }) {
133
+ const { title, giphy } = attachment
134
+ const { url } = giphy.fixedWidth
135
+ const { width, height } = assertKeysAreNumbers(giphy.fixedWidth)
136
+ const styles = useStyles({ imageWidth: width, imageHeight: height })
137
+
138
+ return (
139
+ <Image
140
+ source={{ uri: url }}
141
+ wrapperStyle={styles.imageWrapper}
142
+ style={styles.image}
143
+ alt={title}
144
+ loaderSize={16}
145
+ />
146
+ )
147
+ }
148
+
149
+ function ExpandedLinkImage({
150
+ attachment,
151
+ }: {
152
+ attachment: DenormalizedExpandedLinkAttachmentResource
153
+ }) {
154
+ const { title = '', imageUrl, imageHeight, imageWidth } = attachment.attributes
155
+ const styles = useStyles({ imageWidth, imageHeight })
156
+
157
+ return (
158
+ <Image
159
+ source={{ uri: imageUrl }}
160
+ wrapperStyle={styles.imageWrapper}
161
+ style={styles.image}
162
+ alt={title}
163
+ loaderSize={16}
164
+ />
165
+ )
166
+ }
167
+
168
+ function MessageAttachmentImage({
169
+ attachment,
170
+ }: {
171
+ attachment: DenormalizedMessageAttachmentResource
172
+ }) {
173
+ const { url, urlMedium, filename, metadata = {} } = attachment.attributes
174
+ const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height })
175
+
176
+ return (
177
+ <Image
178
+ source={{ uri: urlMedium || url }}
179
+ style={styles.image}
180
+ wrapperStyle={styles.imageWrapper}
181
+ alt={filename}
182
+ loaderSize={16}
183
+ />
184
+ )
185
+ }
186
+
187
+ function MessageAttachmentIcon({ iconName }: { iconName: IconProps['name'] }) {
188
+ const styles = useStyles()
189
+ return (
190
+ <Icon
191
+ name={iconName}
192
+ style={styles.attachmentIcon}
193
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER}
194
+ />
195
+ )
196
+ }
197
+
198
+ interface StylesProps {
199
+ imageWidth?: number
200
+ imageHeight?: number
201
+ mine?: boolean
202
+ }
203
+
204
+ const useStyles = ({ mine, imageWidth = 32, imageHeight = 32 }: StylesProps = {}) => {
205
+ const { colors } = useTheme()
206
+ const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER })
207
+ const { width } = useWindowDimensions()
208
+ const tabletWidth = width >= 744 // Smallest iPad Mini's width
209
+
210
+ return StyleSheet.create({
211
+ message: {
212
+ gap: 8,
213
+ flexDirection: mine ? 'row-reverse' : 'row',
214
+ paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
215
+ },
216
+ messageContent: {
217
+ flex: 1,
218
+ gap: 4,
219
+ marginBottom: 12,
220
+ },
221
+ avatarWrapper: {
222
+ width: MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
223
+ alignItems: 'center',
224
+ },
225
+ avatar: {
226
+ marginBottom: 8,
227
+ opacity: 0.5,
228
+ },
229
+ messageBubble: {
230
+ flexDirection: 'row',
231
+ alignSelf: mine ? 'flex-end' : 'flex-start',
232
+ alignItems: 'center',
233
+ gap: 8,
234
+ borderColor: colors.borderColorDefaultBase,
235
+ borderWidth: 1,
236
+ borderRadius: 8,
237
+ maxWidth: tabletWidth ? 360 : '80%',
238
+ paddingVertical: 6,
239
+ paddingHorizontal: 8,
240
+ },
241
+ messageText: {
242
+ color: colors.textColorDefaultPlaceholder,
243
+ flexShrink: 1,
244
+ },
245
+ messageMeta: {
246
+ flexDirection: 'row',
247
+ justifyContent: mine ? 'flex-end' : 'flex-start',
248
+ },
249
+ replyCountText: {
250
+ color: colors.interaction,
251
+ fontWeight: platformFontWeightMedium,
252
+ },
253
+ imageWrapper: {
254
+ width: 32 * fontScale,
255
+ aspectRatio: imageWidth / imageHeight,
256
+ opacity: 0.5,
257
+ },
258
+ image: {
259
+ borderRadius: 4,
260
+ },
261
+ attachmentIcon: {
262
+ color: colors.iconColorDefaultDim,
263
+ fontSize: 16,
264
+ },
265
+ })
266
+ }
@@ -45,6 +45,7 @@ export type {
45
45
  // =================================
46
46
 
47
47
  const AVATAR_SIZES = {
48
+ xs: 'xs',
48
49
  sm: 'sm',
49
50
  md: 'md',
50
51
  lg: 'lg',
@@ -60,18 +61,21 @@ type AvatarSize = (typeof AVATAR_SIZES)[keyof typeof AVATAR_SIZES]
60
61
  type AvatarPresenceType = (typeof AVATAR_PRESENCE_TYPES)[keyof typeof AVATAR_PRESENCE_TYPES]
61
62
 
62
63
  const AVATAR_PX: Record<AvatarSize, number> = {
64
+ [AVATAR_SIZES.xs]: 20,
63
65
  [AVATAR_SIZES.sm]: 24,
64
66
  [AVATAR_SIZES.md]: 32,
65
67
  [AVATAR_SIZES.lg]: 40,
66
68
  }
67
69
 
68
70
  const AVATAR_PRESENCE_PX: Record<AvatarSize, number> = {
71
+ [AVATAR_SIZES.xs]: 8,
69
72
  [AVATAR_SIZES.sm]: 10,
70
73
  [AVATAR_SIZES.md]: 12,
71
74
  [AVATAR_SIZES.lg]: 14,
72
75
  }
73
76
 
74
77
  const AVATAR_FALLBACK_ICON_PX: Record<AvatarSize, number> = {
78
+ [AVATAR_SIZES.xs]: 10,
75
79
  [AVATAR_SIZES.sm]: 12,
76
80
  [AVATAR_SIZES.md]: 16,
77
81
  [AVATAR_SIZES.lg]: 20,
@@ -1,4 +1,5 @@
1
1
  export * from './use_async_storage'
2
+ export * from './use_animated_message_background_color'
2
3
  export * from './use_theme'
3
4
  export * from './use_suspense_api'
4
5
  export * from './use_current_person'
@@ -0,0 +1,44 @@
1
+ import { Platform } from 'react-native'
2
+ import {
3
+ useSharedValue,
4
+ useAnimatedStyle,
5
+ withTiming,
6
+ interpolateColor,
7
+ Easing,
8
+ } from 'react-native-reanimated'
9
+ import { useTheme } from '../hooks'
10
+
11
+ export function useAnimatedMessageBackgroundColor() {
12
+ const { colors } = useTheme()
13
+ const bgFadeProgress = useSharedValue(0)
14
+
15
+ const pressedColor = Platform.select({
16
+ ios: colors.fillColorNeutral050Base,
17
+ default: 'transparent',
18
+ })
19
+
20
+ const animatedBackgroundColor = useAnimatedStyle(() => {
21
+ const backgroundColor = interpolateColor(
22
+ bgFadeProgress.value,
23
+ [0, 1],
24
+ ['transparent', pressedColor]
25
+ )
26
+ return {
27
+ backgroundColor,
28
+ }
29
+ })
30
+
31
+ const handleMessagePressIn = () => {
32
+ bgFadeProgress.value = withTiming(1, { duration: 300, easing: Easing.inOut(Easing.ease) })
33
+ }
34
+
35
+ const handleMessagePressOut = () => {
36
+ bgFadeProgress.value = withTiming(0, { duration: 300, easing: Easing.inOut(Easing.ease) })
37
+ }
38
+
39
+ return {
40
+ animatedBackgroundColor,
41
+ handleMessagePressIn,
42
+ handleMessagePressOut,
43
+ }
44
+ }
@@ -0,0 +1,13 @@
1
+ export const assertKeysAreNumbers = (obj: Object): Record<any, number> => {
2
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, assertNumber(value)]))
3
+ }
4
+
5
+ const assertNumber = (value: string): number => {
6
+ if (typeof value === 'number') return value
7
+
8
+ if (typeof value === 'string' && !isNaN(Number(value))) {
9
+ return Number(value)
10
+ }
11
+
12
+ return 0
13
+ }
@@ -8,3 +8,4 @@ export * from './native_adapters'
8
8
  export * from './pluralize'
9
9
  export * from './destructure_chat_group_graph_id'
10
10
  export * from './convert_attachments_for_create'
11
+ export * from './assert_keys_are_numbers'