@helpwave/hightide-native 0.6.0 → 0.7.0

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 (86) hide show
  1. package/package.json +3 -3
  2. package/src/components/card/Card.tsx +2 -4
  3. package/src/components/chat/ChatAttachmentMessageBubble.tsx +152 -90
  4. package/src/components/chat/ChatConversationList.tsx +4 -12
  5. package/src/components/chat/ChatConversationRow.tsx +159 -164
  6. package/src/components/chat/ChatDateDivider.tsx +3 -8
  7. package/src/components/chat/ChatMessageBubble.tsx +51 -45
  8. package/src/components/chat/ChatMessageComposer.tsx +4 -12
  9. package/src/components/chat/ChatMessageList.tsx +3 -9
  10. package/src/components/chat/ChatQuickReplyChip.tsx +41 -27
  11. package/src/components/chat/ChatSystemLine.tsx +4 -12
  12. package/src/components/chat/ChatThreadHeader.tsx +136 -101
  13. package/src/components/layout/Divider.tsx +2 -4
  14. package/src/components/list/ListActionItem.tsx +57 -65
  15. package/src/components/list/ListItem.tsx +8 -28
  16. package/src/components/list/ListItemTextContent.tsx +4 -2
  17. package/src/components/list/ListNavigationItem.tsx +55 -63
  18. package/src/components/user-interaction/Button.tsx +43 -49
  19. package/src/components/user-interaction/Checkbox.tsx +47 -48
  20. package/src/components/user-interaction/IconButton.tsx +40 -42
  21. package/src/components/user-interaction/Input.tsx +69 -80
  22. package/src/components/user-interaction/MultiSelect/MultiSelect.tsx +40 -0
  23. package/src/components/user-interaction/MultiSelect/MultiSelectContext.tsx +70 -0
  24. package/src/components/user-interaction/MultiSelect/MultiSelectMenu.tsx +87 -0
  25. package/src/components/user-interaction/MultiSelect/MultiSelectOption.tsx +87 -0
  26. package/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx +194 -0
  27. package/src/components/user-interaction/MultiSelect/MultiSelectTrigger.tsx +123 -0
  28. package/src/components/user-interaction/MultiSelect/index.ts +6 -0
  29. package/src/components/user-interaction/SearchBar.tsx +11 -17
  30. package/src/components/user-interaction/Select/Select.tsx +40 -0
  31. package/src/components/user-interaction/Select/SelectContext.tsx +69 -0
  32. package/src/components/user-interaction/Select/SelectMenu.tsx +87 -0
  33. package/src/components/user-interaction/Select/SelectOption.tsx +77 -0
  34. package/src/components/user-interaction/Select/SelectRoot.tsx +186 -0
  35. package/src/components/user-interaction/Select/SelectTrigger.tsx +92 -0
  36. package/src/components/user-interaction/Select/index.ts +6 -0
  37. package/src/components/user-interaction/Switch.tsx +46 -98
  38. package/src/components/user-interaction/Textarea.tsx +165 -12
  39. package/src/components/user-interaction/ThemedPressable.tsx +48 -49
  40. package/src/components/visualization-and-display/Avatar.tsx +19 -17
  41. package/src/components/visualization-and-display/Chip.tsx +4 -12
  42. package/src/components/visualization-and-display/ThemedText.tsx +12 -5
  43. package/src/enums/chatMessageDirection.ts +18 -0
  44. package/src/enums/chatMessageStatus.ts +18 -0
  45. package/src/enums/hightideThemeModes.ts +18 -0
  46. package/src/enums/index.ts +7 -0
  47. package/src/enums/listItemContentOrder.ts +18 -0
  48. package/src/enums/themedTextAppearance.ts +18 -0
  49. package/src/enums/useMultiSelectFirstHighlightBehavior.ts +18 -0
  50. package/src/enums/useSelectFirstHighlightBehavior.ts +18 -0
  51. package/src/global-contexts/content-theme/ContentThemeProvider.tsx +9 -5
  52. package/src/global-contexts/theme/ThemeProvider.tsx +4 -1
  53. package/src/hooks/index.ts +2 -0
  54. package/src/hooks/useAnimatedStyleTransition.ts +460 -0
  55. package/src/hooks/useMemoizedTheme.ts +23 -0
  56. package/src/hooks/useMultiSelect.ts +3 -1
  57. package/src/hooks/useSelect.ts +3 -1
  58. package/src/icons/HightideIconRegistry.ts +2 -0
  59. package/src/theme/adapters/style-adapter-utils.ts +121 -27
  60. package/src/theme/resolvers/avatar.ts +48 -39
  61. package/src/theme/resolvers/button.ts +7 -17
  62. package/src/theme/resolvers/chat/attachment-message-bubble.ts +5 -6
  63. package/src/theme/resolvers/chat/conversation-row.ts +4 -6
  64. package/src/theme/resolvers/chat/message-bubble.ts +1 -2
  65. package/src/theme/resolvers/chat/message-composer.ts +1 -2
  66. package/src/theme/resolvers/chat/quick-reply-chip.ts +2 -3
  67. package/src/theme/resolvers/chat/system-line.ts +1 -2
  68. package/src/theme/resolvers/chat/thread-header.ts +3 -4
  69. package/src/theme/resolvers/checkbox.ts +2 -2
  70. package/src/theme/resolvers/chip.ts +1 -2
  71. package/src/theme/resolvers/iconButton.ts +4 -13
  72. package/src/theme/resolvers/input.ts +30 -14
  73. package/src/theme/resolvers/listItem.ts +2 -3
  74. package/src/theme/resolvers/multiSelect.ts +1 -2
  75. package/src/theme/resolvers/searchBar.ts +1 -2
  76. package/src/theme/resolvers/select.ts +1 -2
  77. package/src/theme/resolvers/themedPressable.ts +4 -15
  78. package/src/theme/themes/hightideThemes.ts +3 -1
  79. package/src/theme/types/components/chat.ts +6 -21
  80. package/src/theme/types/components/input.ts +3 -0
  81. package/src/theme/types/components/textarea.ts +1 -0
  82. package/src/theme/types/resolver.ts +4 -27
  83. package/src/utils/index.ts +1 -0
  84. package/src/utils/pressableInteraction.ts +7 -0
  85. package/src/components/user-interaction/MultiSelect.tsx +0 -282
  86. package/src/components/user-interaction/Select.tsx +0 -242
@@ -1,12 +1,10 @@
1
1
  import {
2
- cloneElement,
3
- Fragment,
4
- isValidElement,
5
2
  useMemo,
6
- type ReactElement,
3
+ useState,
7
4
  type ReactNode
8
5
  } from 'react'
9
6
  import {
7
+ StyleSheet,
10
8
  View,
11
9
  type PressableProps
12
10
  } from 'react-native'
@@ -14,9 +12,10 @@ import { HightideIconRegistry } from '../../icons/HightideIconRegistry'
14
12
  import { ThemedIcon } from '../visualization-and-display/ThemedIcon'
15
13
  import { ThemedText } from '../visualization-and-display/ThemedText'
16
14
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
15
+ import { useMemoizedTheme, useMemoizedThemeFactory } from '../../hooks/useMemoizedTheme'
17
16
  import type {
18
17
  AvatarState,
19
- AvatarStyle
18
+ AvatarThemeResolvers
20
19
  } from '../../theme/types/components/avatar'
21
20
  import type {
22
21
  ChatConversationRowContentContainerStyle,
@@ -28,22 +27,32 @@ import type {
28
27
  ChatConversationRowTimestampStyle,
29
28
  ChatConversationRowTitleStyle,
30
29
  PressableContainerStyle,
31
- PressableState
30
+ PressableIconStyle,
31
+ PressableState,
32
+ PressableStateLayerStyle,
33
+ PressableTextStyle
32
34
  } from '../../theme/types/components/chat'
33
- import type { StyleOverwrite } from '../../theme/types/resolver'
35
+ import type { StyleOverwrite, StyleResolverFunction } from '../../theme/types/resolver'
36
+ import {
37
+ AvatarWithStatus,
38
+ type AvatarWithStatusProps
39
+ } from '../visualization-and-display/Avatar'
40
+
41
+ import type { ChatMessageStatus } from '../../enums/chatMessageStatus'
42
+ import type { IconComponent } from '../../icons/types'
34
43
  import { ThemedPressable } from '../user-interaction'
35
- import type { AvatarProps } from '../visualization-and-display/Avatar'
36
44
 
37
- export type ChatConversationSentIndicator = 'sent' | 'sentAndReceived'
45
+ export type { ChatMessageStatus }
38
46
 
39
47
  export type ChatConversationRowProps = Omit<PressableProps, 'children' | 'style'> & {
40
- avatar: ReactNode,
48
+ avatarProps?: AvatarWithStatusProps,
49
+ avatarOverride?: ReactNode,
41
50
  title: ReactNode,
42
51
  timestamp?: ReactNode,
43
52
  preview?: ReactNode,
44
53
  unreadCount?: number,
45
54
  isSelected?: boolean,
46
- sentIndicator?: ChatConversationSentIndicator,
55
+ messageStatus?: ChatMessageStatus,
47
56
  style?: StyleOverwrite<PressableState, PressableContainerStyle>,
48
57
  contentContainerStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowContentContainerStyle>,
49
58
  headerRowStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowHeaderRowStyle>,
@@ -52,32 +61,34 @@ export type ChatConversationRowProps = Omit<PressableProps, 'children' | 'style'
52
61
  timestampStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowTimestampStyle>,
53
62
  previewStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowPreviewStyle>,
54
63
  sentIndicatorStyle?: StyleOverwrite<Record<string, never>, ChatConversationRowSentIndicatorStyle>,
55
- avatarStyle?: StyleOverwrite<AvatarState, AvatarStyle>,
56
64
  }
57
65
 
58
- type PressableInteraction = {
59
- pressed: boolean,
60
- hovered?: boolean,
61
- focused?: boolean,
62
- focusVisible?: boolean,
63
- }
64
-
65
- type AvatarElementProps = AvatarProps & {
66
- avatarStyle?: AvatarProps['avatarStyle'],
66
+ type ConversationRowPressableResolvers = {
67
+ container: StyleResolverFunction<PressableState, PressableContainerStyle>,
68
+ stateLayer: StyleResolverFunction<PressableState, PressableStateLayerStyle>,
69
+ text: StyleResolverFunction<PressableState, PressableTextStyle>,
70
+ icon: StyleResolverFunction<PressableState, PressableIconStyle>,
67
71
  }
68
72
 
69
73
  const toNumericSize = (value: unknown): number | undefined => (
70
74
  typeof value === 'number' ? value : undefined
71
75
  )
72
76
 
77
+ const resolveMessageStatusIcon = (messageStatus: ChatMessageStatus): IconComponent => (
78
+ messageStatus === 'sent'
79
+ ? HightideIconRegistry.Clock
80
+ : HightideIconRegistry.CheckCheck
81
+ )
82
+
73
83
  export const ChatConversationRow = ({
74
- avatar,
84
+ avatarProps,
85
+ avatarOverride,
75
86
  title,
76
87
  timestamp,
77
88
  preview,
78
89
  unreadCount,
79
90
  isSelected = false,
80
- sentIndicator,
91
+ messageStatus,
81
92
  disabled,
82
93
  style,
83
94
  contentContainerStyle,
@@ -87,169 +98,153 @@ export const ChatConversationRow = ({
87
98
  timestampStyle,
88
99
  previewStyle,
89
100
  sentIndicatorStyle,
90
- avatarStyle,
91
101
  ...props
92
102
  }: ChatConversationRowProps) => {
93
103
  const { theme } = useTheme()
104
+ const [isPressed, setIsPressed] = useState(false)
94
105
  const isUnread = (unreadCount ?? 0) > 0
95
- const sentIndicatorIcon = sentIndicator === 'sentAndReceived'
96
- ? HightideIconRegistry.CheckCheck
97
- : HightideIconRegistry.Check
106
+ const staticState = useMemo(() => ({}), [])
98
107
 
99
- const resolveState = (interaction: PressableInteraction): ChatConversationRowState => ({
108
+ const rowState = useMemo((): ChatConversationRowState => ({
100
109
  isUnread,
101
110
  isSelected,
102
111
  isDisabled: !!disabled,
103
- isPressed: interaction.pressed,
104
- isHovered: !!interaction.hovered,
105
- isFocused: !!interaction.focused,
106
- isFocusVisible: !!interaction.focusVisible,
107
- })
112
+ isPressed,
113
+ }), [disabled, isPressed, isSelected, isUnread])
108
114
 
109
- const staticState = useMemo(() => ({}), [])
110
- const unreadBadge = useMemo(
111
- () => theme.components.chat.conversationRow.unreadBadge(staticState),
112
- [theme, staticState]
113
- )
114
- const unreadBadgeText = useMemo(
115
- () => theme.components.chat.conversationRow.unreadBadgeText(staticState),
116
- [theme, staticState]
117
- )
118
- const resolvedSentIndicator = useMemo(
119
- () => theme.components.chat.conversationRow.sentIndicator(staticState, sentIndicatorStyle),
120
- [theme, staticState, sentIndicatorStyle]
121
- )
122
- const avatarTheme = useMemo(
123
- () => theme.components.chat.conversationRow.avatar(staticState),
124
- [theme, staticState]
115
+ const pressableThemeState = useMemo((): PressableState => ({
116
+ isPressed,
117
+ isDisabled: !!disabled,
118
+ }), [disabled, isPressed])
119
+
120
+ const pressableResolvers = useMemoizedThemeFactory<
121
+ ChatConversationRowState,
122
+ ConversationRowPressableResolvers
123
+ >(theme.components.chat.conversationRow.pressable, rowState)
124
+
125
+ const resolvedContainerStyle = useMemoizedTheme(pressableResolvers.container, pressableThemeState, style)
126
+ const resolvedStateLayerStyle = useMemoizedTheme(pressableResolvers.stateLayer, pressableThemeState)
127
+ const resolvedContentContainer = useMemoizedTheme(theme.components.chat.conversationRow.contentContainer, rowState, contentContainerStyle)
128
+ const resolvedHeaderRow = useMemoizedTheme(theme.components.chat.conversationRow.headerRow, rowState, headerRowStyle)
129
+ const resolvedMessageRow = useMemoizedTheme(theme.components.chat.conversationRow.messageRow, rowState, messageRowStyle)
130
+ const resolvedTitle = useMemoizedTheme(theme.components.chat.conversationRow.title, rowState, titleStyle)
131
+ const resolvedTimestamp = useMemoizedTheme(theme.components.chat.conversationRow.timestamp, rowState, timestampStyle)
132
+ const resolvedPreview = useMemoizedTheme(theme.components.chat.conversationRow.preview, rowState, previewStyle)
133
+ const unreadBadge = useMemoizedTheme(theme.components.chat.conversationRow.unreadBadge, staticState)
134
+ const unreadBadgeText = useMemoizedTheme(theme.components.chat.conversationRow.unreadBadgeText, staticState)
135
+ const resolvedSentIndicator = useMemoizedTheme(theme.components.chat.conversationRow.sentIndicator, staticState, sentIndicatorStyle)
136
+ const avatarTheme = useMemoizedThemeFactory<AvatarState, AvatarThemeResolvers>(
137
+ theme.components.chat.conversationRow.avatar,
138
+ staticState
125
139
  )
126
140
  const avatarSize = useMemo(
127
- () => toNumericSize(avatarTheme.container({}).width),
128
- [avatarTheme]
141
+ () => toNumericSize(StyleSheet.flatten(avatarTheme.container({})).width) ?? avatarProps?.size,
142
+ [avatarProps?.size, avatarTheme]
129
143
  )
144
+ const messageStatusIcon = messageStatus === undefined
145
+ ? undefined
146
+ : resolveMessageStatusIcon(messageStatus)
147
+ const messageStatusIconColor = messageStatus === 'read'
148
+ ? theme.colors.primary.color
149
+ : resolvedSentIndicator.color
130
150
 
131
- const resolvedAvatar = useMemo(() => {
132
- if (!isValidElement(avatar) || avatarSize === undefined) {
133
- return avatar
134
- }
135
-
136
- const avatarElement = avatar as ReactElement<AvatarElementProps>
137
-
138
- return cloneElement(avatarElement, {
139
- size: avatarSize,
140
- avatarStyle: (avatarState) => avatarTheme.container(
141
- {
142
- ...avatarState,
143
- size: avatarSize,
144
- },
145
- avatarStyle ?? avatarElement.props.avatarStyle
146
- ),
147
- imageStyle: (avatarState) => avatarTheme.image(
148
- {
149
- ...avatarState,
150
- size: avatarSize,
151
- },
152
- avatarElement.props.imageStyle
153
- ),
154
- textStyle: (avatarState) => avatarTheme.text(
155
- {
156
- ...avatarState,
157
- size: avatarSize,
158
- },
159
- avatarElement.props.textStyle
160
- ),
161
- iconStyle: (avatarState) => avatarTheme.icon(
162
- {
163
- ...avatarState,
164
- size: avatarSize,
165
- },
166
- avatarElement.props.iconStyle
167
- ),
168
- })
169
- }, [avatar, avatarSize, avatarStyle, avatarTheme])
151
+ const {
152
+ avatarStyle,
153
+ imageStyle,
154
+ textStyle,
155
+ iconStyle,
156
+ ...restAvatarProps
157
+ } = avatarProps ?? {}
170
158
 
171
159
  return (
172
160
  <ThemedPressable
173
161
  {...props}
174
162
  disabled={disabled}
175
- style={(pressableState) => {
176
- const state = resolveState(pressableState as PressableInteraction)
177
- return theme.components.chat.conversationRow.pressable(state).container(
178
- pressableState,
179
- style
180
- )
181
- }}
182
- stateLayerStyle={(pressableState) => {
183
- const state = resolveState(pressableState as PressableInteraction)
184
- return theme.components.chat.conversationRow.pressable(state).stateLayer(pressableState)
185
- }}
186
- textStyle={(pressableState) => {
187
- const state = resolveState(pressableState as PressableInteraction)
188
- return theme.components.chat.conversationRow.pressable(state).text(pressableState)
163
+ style={resolvedContainerStyle}
164
+ stateLayerStyle={resolvedStateLayerStyle}
165
+ onPressIn={(event) => {
166
+ setIsPressed(true)
167
+ props.onPressIn?.(event)
189
168
  }}
190
- iconStyle={(pressableState) => {
191
- const state = resolveState(pressableState as PressableInteraction)
192
- return theme.components.chat.conversationRow.pressable(state).icon(pressableState)
169
+ onPressOut={(event) => {
170
+ setIsPressed(false)
171
+ props.onPressOut?.(event)
193
172
  }}
194
173
  >
195
- {(pressableState) => {
196
- const state = resolveState(pressableState as PressableInteraction)
197
- const resolvedContentContainer = theme.components.chat.conversationRow.contentContainer(
198
- state,
199
- contentContainerStyle
200
- )
201
- const resolvedHeaderRow = theme.components.chat.conversationRow.headerRow(state, headerRowStyle)
202
- const resolvedMessageRow = theme.components.chat.conversationRow.messageRow(state, messageRowStyle)
203
- const resolvedTitle = theme.components.chat.conversationRow.title(state, titleStyle)
204
- const resolvedTimestamp = theme.components.chat.conversationRow.timestamp(state, timestampStyle)
205
- const resolvedPreview = theme.components.chat.conversationRow.preview(state, previewStyle)
206
-
207
- return (
208
- <Fragment>
209
- {resolvedAvatar}
210
- <View style={resolvedContentContainer}>
211
- <View style={resolvedHeaderRow}>
212
- {typeof title === 'string' || typeof title === 'number' ? (
213
- <ThemedText style={resolvedTitle} numberOfLines={1}>{title}</ThemedText>
214
- ) : (
215
- title
216
- )}
217
- {timestamp != null && (
218
- typeof timestamp === 'string' || typeof timestamp === 'number' ? (
219
- <ThemedText style={resolvedTimestamp}>{timestamp}</ThemedText>
220
- ) : (
221
- timestamp
222
- )
223
- )}
224
- </View>
225
- <View style={resolvedMessageRow}>
226
- <View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, gap: theme.spacing.xs /* TODO replace this with a theme style */ }}>
227
- {sentIndicator && (
228
- <ThemedIcon
229
- icon={sentIndicatorIcon}
230
- size={resolvedSentIndicator.size}
231
- strokeWidth={resolvedSentIndicator.strokeWidth}
232
- color={resolvedSentIndicator.color}
233
- />
234
- )}
235
- {preview != null && (
236
- typeof preview === 'string' || typeof preview === 'number' ? (
237
- <ThemedText style={resolvedPreview} numberOfLines={1}>{preview}</ThemedText>
238
- ) : (
239
- preview
240
- )
241
- )}
242
- </View>
243
- {isUnread && (
244
- <View style={unreadBadge}>
245
- <ThemedText style={unreadBadgeText}>{unreadCount}</ThemedText>
246
- </View>
247
- )}
248
- </View>
174
+ {avatarOverride ?? (
175
+ <AvatarWithStatus
176
+ {...restAvatarProps}
177
+ size={avatarSize}
178
+ avatarStyle={(avatarState) => avatarTheme.container(
179
+ {
180
+ ...avatarState,
181
+ size: avatarSize,
182
+ },
183
+ avatarStyle
184
+ )}
185
+ imageStyle={(avatarState) => avatarTheme.image(
186
+ {
187
+ ...avatarState,
188
+ size: avatarSize,
189
+ },
190
+ imageStyle
191
+ )}
192
+ textStyle={(avatarState) => avatarTheme.text(
193
+ {
194
+ ...avatarState,
195
+ size: avatarSize,
196
+ },
197
+ textStyle
198
+ )}
199
+ iconStyle={(avatarState) => avatarTheme.icon(
200
+ {
201
+ ...avatarState,
202
+ size: avatarSize,
203
+ },
204
+ iconStyle
205
+ )}
206
+ />
207
+ )}
208
+ <View style={resolvedContentContainer}>
209
+ <View style={resolvedHeaderRow}>
210
+ {typeof title === 'string' || typeof title === 'number' ? (
211
+ <ThemedText style={resolvedTitle} numberOfLines={1}>{title}</ThemedText>
212
+ ) : (
213
+ title
214
+ )}
215
+ {timestamp != null && (
216
+ typeof timestamp === 'string' || typeof timestamp === 'number' ? (
217
+ <ThemedText style={resolvedTimestamp}>{timestamp}</ThemedText>
218
+ ) : (
219
+ timestamp
220
+ )
221
+ )}
222
+ </View>
223
+ <View style={resolvedMessageRow}>
224
+ <View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, gap: theme.spacing.xs }}>
225
+ {messageStatusIcon != null && (
226
+ <ThemedIcon
227
+ icon={messageStatusIcon}
228
+ size={resolvedSentIndicator.size}
229
+ strokeWidth={resolvedSentIndicator.strokeWidth}
230
+ color={messageStatusIconColor}
231
+ />
232
+ )}
233
+ {preview != null && (
234
+ typeof preview === 'string' || typeof preview === 'number' ? (
235
+ <ThemedText style={[resolvedPreview, { flex: 1 }]} numberOfLines={1}>{preview}</ThemedText>
236
+ ) : (
237
+ preview
238
+ )
239
+ )}
240
+ </View>
241
+ {isUnread && (
242
+ <View style={unreadBadge}>
243
+ <ThemedText style={unreadBadgeText}>{unreadCount}</ThemedText>
249
244
  </View>
250
- </Fragment>
251
- )
252
- }}
245
+ )}
246
+ </View>
247
+ </View>
253
248
  </ThemedPressable>
254
249
  )
255
250
  }
@@ -10,6 +10,7 @@ import {
10
10
  } from 'react-native'
11
11
 
12
12
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
13
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
13
14
  import { ThemedText } from '../visualization-and-display/ThemedText'
14
15
  import type {
15
16
  ChatDateDividerStyle,
@@ -34,14 +35,8 @@ export const ChatDateDivider = ({
34
35
  const { theme } = useTheme()
35
36
  const state = useMemo(() => ({}), [])
36
37
 
37
- const resolvedDividerStyle = useMemo(
38
- () => theme.components.chat.dateDivider.container(state, dividerStyle),
39
- [theme, state, dividerStyle]
40
- )
41
- const resolvedTextStyle = useMemo(
42
- () => theme.components.chat.dateDivider.text(state, textStyle),
43
- [theme, state, textStyle]
44
- )
38
+ const resolvedDividerStyle = useMemoizedTheme(theme.components.chat.dateDivider.container, state, dividerStyle)
39
+ const resolvedTextStyle = useMemoizedTheme(theme.components.chat.dateDivider.text, state, textStyle)
45
40
 
46
41
  return (
47
42
  <View {...props} style={[resolvedDividerStyle, style]}>
@@ -6,10 +6,22 @@ import {
6
6
  View,
7
7
  type ViewProps
8
8
  } from 'react-native'
9
+ import { DateUtils } from '@helpwave/hightide-utils/utils'
9
10
  import { HightideIconRegistry } from '../../icons/HightideIconRegistry'
10
11
  import { ThemedIcon } from '../visualization-and-display/ThemedIcon'
11
12
  import { ThemedText } from '../visualization-and-display/ThemedText'
12
13
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
14
+ import {
15
+ useDateTimeFormat,
16
+ useLocalization
17
+ } from '../../global-contexts/localization/forward-exports'
18
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
19
+ import type {
20
+ ChatMessageStatus
21
+ } from '../../enums/chatMessageStatus'
22
+ import type {
23
+ ChatMessageDirection
24
+ } from '../../enums/chatMessageDirection'
13
25
  import type {
14
26
  ChatMessageBubbleBodyStyle,
15
27
  ChatMessageBubbleBodyTextStyle,
@@ -17,17 +29,17 @@ import type {
17
29
  ChatMessageBubbleMetaDataContainerStyle,
18
30
  ChatMessageBubbleMetaDataStatusContainerStyle,
19
31
  ChatMessageBubbleMetaDataTextStyle,
20
- ChatMessageBubbleState,
21
- ChatMessageDirection
32
+ ChatMessageBubbleState
22
33
  } from '../../theme/types/components/chat'
23
34
  import type { StyleOverwrite } from '../../theme/types/resolver'
35
+ import type { IconComponent } from '../../icons/types'
24
36
 
25
- export type { ChatMessageDirection }
37
+ export type { ChatMessageDirection, ChatMessageStatus }
26
38
 
27
39
  export type ChatMessageBubbleProps = Omit<ViewProps, 'children' | 'style'> & {
28
40
  direction: ChatMessageDirection,
29
- timestamp: ReactNode,
30
- readReceipt?: ReactNode,
41
+ timestamp?: Date,
42
+ messageStatus?: ChatMessageStatus,
31
43
  children?: ReactNode,
32
44
  style?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleContainerStyle>,
33
45
  bodyStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleBodyStyle>,
@@ -37,10 +49,16 @@ export type ChatMessageBubbleProps = Omit<ViewProps, 'children' | 'style'> & {
37
49
  metaDataTextStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleMetaDataTextStyle>,
38
50
  }
39
51
 
52
+ const resolveMessageStatusIcon = (messageStatus: ChatMessageStatus): IconComponent => (
53
+ messageStatus === 'sent'
54
+ ? HightideIconRegistry.Clock
55
+ : HightideIconRegistry.CheckCheck
56
+ )
57
+
40
58
  export const ChatMessageBubble = ({
41
59
  direction,
42
60
  timestamp,
43
- readReceipt,
61
+ messageStatus,
44
62
  children,
45
63
  style,
46
64
  bodyStyle,
@@ -51,36 +69,31 @@ export const ChatMessageBubble = ({
51
69
  ...props
52
70
  }: ChatMessageBubbleProps) => {
53
71
  const { theme } = useTheme()
72
+ const { locale } = useLocalization()
73
+ const { is24HourFormat, timeZone } = useDateTimeFormat()
54
74
  const state = useMemo(() => ({ direction }), [direction])
55
75
 
56
- const resolvedContainerStyle = useMemo(
57
- () => theme.components.chat.messageBubble.container(state, style),
58
- [theme, state, style]
59
- )
60
- const resolvedBodyStyle = useMemo(
61
- () => theme.components.chat.messageBubble.body(state, bodyStyle),
62
- [theme, state, bodyStyle]
63
- )
64
- const resolvedBodyTextStyle = useMemo(
65
- () => theme.components.chat.messageBubble.bodyText(state, bodyTextStyle),
66
- [theme, state, bodyTextStyle]
67
- )
68
- const resolvedMetaDataContainerStyle = useMemo(
69
- () => theme.components.chat.messageBubble.metaDataContainer(state, metaDataContainerStyle),
70
- [theme, state, metaDataContainerStyle]
71
- )
72
- const resolvedMetaDataStatusContainerStyle = useMemo(
73
- () => theme.components.chat.messageBubble.metaDataStatusContainer(state, metaDataStatusContainerStyle),
74
- [theme, state, metaDataStatusContainerStyle]
75
- )
76
- const resolvedMetaDataTextStyle = useMemo(
77
- () => theme.components.chat.messageBubble.metaDataText(state, metaDataTextStyle),
78
- [theme, state, metaDataTextStyle]
79
- )
80
- const resolvedMetaDataIcon = useMemo(
81
- () => theme.components.chat.messageBubble.metaDataIcon(state),
82
- [theme, state]
76
+ const resolvedContainerStyle = useMemoizedTheme(theme.components.chat.messageBubble.container, state, style)
77
+ const resolvedBodyStyle = useMemoizedTheme(theme.components.chat.messageBubble.body, state, bodyStyle)
78
+ const resolvedBodyTextStyle = useMemoizedTheme(theme.components.chat.messageBubble.bodyText, state, bodyTextStyle)
79
+ const resolvedMetaDataContainerStyle = useMemoizedTheme(theme.components.chat.messageBubble.metaDataContainer, state, metaDataContainerStyle)
80
+ const resolvedMetaDataStatusContainerStyle = useMemoizedTheme(theme.components.chat.messageBubble.metaDataStatusContainer, state, metaDataStatusContainerStyle)
81
+ const resolvedMetaDataTextStyle = useMemoizedTheme(theme.components.chat.messageBubble.metaDataText, state, metaDataTextStyle)
82
+ const resolvedMetaDataIcon = useMemoizedTheme(theme.components.chat.messageBubble.metaDataIcon, state)
83
+ const formattedTimestamp = useMemo(() => (
84
+ timestamp === undefined
85
+ ? undefined
86
+ : DateUtils.formatAbsolute(timestamp, locale, 'time', { timeZone, is24HourFormat })
87
+ ), [timestamp, locale, timeZone, is24HourFormat])
88
+ const messageStatusIcon = useMemo(
89
+ () => (messageStatus === undefined ? undefined : resolveMessageStatusIcon(messageStatus)),
90
+ [messageStatus]
83
91
  )
92
+ const messageStatusIconColor = useMemo(() => (
93
+ messageStatus === 'read'
94
+ ? theme.colors.primary.color
95
+ : resolvedMetaDataIcon.color
96
+ ), [messageStatus, resolvedMetaDataIcon.color, theme.colors.primary.color])
84
97
 
85
98
  return (
86
99
  <View {...props} style={resolvedContainerStyle}>
@@ -92,25 +105,18 @@ export const ChatMessageBubble = ({
92
105
  )}
93
106
  </View>
94
107
  <View style={resolvedMetaDataContainerStyle}>
95
- {readReceipt != null && (
108
+ {messageStatusIcon != null && (
96
109
  <View style={resolvedMetaDataStatusContainerStyle}>
97
110
  <ThemedIcon
98
- icon={HightideIconRegistry.CheckCheck}
111
+ icon={messageStatusIcon}
99
112
  size={resolvedMetaDataIcon.size}
100
113
  strokeWidth={resolvedMetaDataIcon.strokeWidth}
101
- color={resolvedMetaDataIcon.color}
114
+ color={messageStatusIconColor}
102
115
  />
103
- {typeof readReceipt === 'string' || typeof readReceipt === 'number' ? (
104
- <ThemedText style={resolvedMetaDataTextStyle}>{readReceipt}</ThemedText>
105
- ) : (
106
- readReceipt
107
- )}
108
116
  </View>
109
117
  )}
110
- {typeof timestamp === 'string' || typeof timestamp === 'number' ? (
111
- <ThemedText style={resolvedMetaDataTextStyle}>{timestamp}</ThemedText>
112
- ) : (
113
- timestamp
118
+ {formattedTimestamp != null && (
119
+ <ThemedText style={resolvedMetaDataTextStyle}>{formattedTimestamp}</ThemedText>
114
120
  )}
115
121
  </View>
116
122
  </View>
@@ -15,6 +15,7 @@ import { useControlledState } from '@helpwave/hightide-utils/hooks'
15
15
  import { HightideIconRegistry } from '../../icons/HightideIconRegistry'
16
16
  import { IconButton } from '../user-interaction/IconButton'
17
17
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
18
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
18
19
  import type {
19
20
  ChatMessageComposerInputStyle,
20
21
  ChatMessageComposerStyle
@@ -60,18 +61,9 @@ export const ChatMessageComposer = ({
60
61
  })
61
62
  const state = useMemo(() => ({}), [])
62
63
 
63
- const resolvedComposerStyle = useMemo(
64
- () => theme.components.chat.messageComposer.container(state, composerStyle),
65
- [theme, state, composerStyle]
66
- )
67
- const resolvedInputStyle = useMemo(
68
- () => theme.components.chat.messageComposer.input(state, inputStyle),
69
- [theme, state, inputStyle]
70
- )
71
- const placeholderColor = useMemo(
72
- () => theme.components.chat.messageComposer.placeholderColor(state),
73
- [theme, state]
74
- )
64
+ const resolvedComposerStyle = useMemoizedTheme(theme.components.chat.messageComposer.container, state, composerStyle)
65
+ const resolvedInputStyle = useMemoizedTheme(theme.components.chat.messageComposer.input, state, inputStyle)
66
+ const placeholderColor = useMemoizedTheme(theme.components.chat.messageComposer.placeholderColor, state)
75
67
 
76
68
  const send = () => {
77
69
  const trimmed = (value ?? '').trim()
@@ -13,6 +13,7 @@ import {
13
13
  } from 'react-native'
14
14
 
15
15
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
16
+ import { useMemoizedTheme } from '../../hooks/useMemoizedTheme'
16
17
  import type { ChatMessageListStyle } from '../../theme/types/components/chat'
17
18
  import type { StyleOverwrite } from '../../theme/types/resolver'
18
19
 
@@ -37,14 +38,7 @@ export const ChatMessageList = ({
37
38
  const scrollRef = useRef<ScrollView>(null)
38
39
  const state = useMemo(() => ({}), [])
39
40
 
40
- const resolvedListStyle = useMemo(
41
- () => theme.components.chat.messageList.container(state, listStyle),
42
- [theme, state, listStyle]
43
- )
44
- const listGap = useMemo(
45
- () => (theme.components.chat.messageList.container(state) as ViewStyle).gap,
46
- [theme, state]
47
- )
41
+ const resolvedListStyle = useMemoizedTheme(theme.components.chat.messageList.container, state, listStyle)
48
42
 
49
43
  useEffect(() => {
50
44
  if (autoScroll) {
@@ -56,7 +50,7 @@ export const ChatMessageList = ({
56
50
  <ScrollView
57
51
  ref={scrollRef}
58
52
  style={[resolvedListStyle, style]}
59
- contentContainerStyle={[{ gap: listGap }, contentContainerStyle]}
53
+ contentContainerStyle={[{ gap: resolvedListStyle.gap }, contentContainerStyle]}
60
54
  onScroll={onScroll}
61
55
  scrollEventThrottle={16}
62
56
  >