@helpwave/hightide-native 0.0.1 → 0.0.5
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.
- package/README.md +56 -0
- package/package.json +3 -3
- package/src/components/chat/ChatAttachmentCard.tsx +110 -0
- package/src/components/chat/ChatConversationList.tsx +72 -0
- package/src/components/chat/ChatConversationRow.tsx +153 -0
- package/src/components/chat/ChatDateDivider.tsx +55 -0
- package/src/components/chat/ChatMessageBubble.tsx +112 -0
- package/src/components/chat/ChatMessageCard.tsx +118 -0
- package/src/components/chat/ChatMessageComposer.tsx +115 -0
- package/src/components/chat/ChatMessageList.tsx +66 -0
- package/src/components/chat/ChatQuickReplyChip.tsx +72 -0
- package/src/components/chat/ChatSystemLine.tsx +68 -0
- package/src/components/chat/ChatThreadHeader.tsx +90 -0
- package/src/components/chat/index.ts +11 -0
- package/src/components/index.ts +4 -7
- package/src/components/menu/Menu.tsx +61 -0
- package/src/components/menu/MenuActionItem.tsx +90 -0
- package/src/components/menu/MenuItem.tsx +73 -0
- package/src/components/menu/MenuNavigationItem.tsx +90 -0
- package/src/components/menu/index.ts +4 -0
- package/src/components/user-interaction/Button.tsx +101 -0
- package/src/components/{Checkbox → user-interaction}/Checkbox.tsx +38 -16
- package/src/components/user-interaction/IconButton.tsx +95 -0
- package/src/components/{Input → user-interaction}/Input.tsx +35 -14
- package/src/components/{MultiSelect → user-interaction}/MultiSelect.tsx +47 -15
- package/src/components/{Select → user-interaction}/Select.tsx +44 -12
- package/src/components/user-interaction/index.ts +6 -0
- package/src/components/{Chip → visualization-and-display}/Chip.tsx +36 -20
- package/src/components/visualization-and-display/Icon.tsx +27 -0
- package/src/components/{Chip → visualization-and-display}/index.ts +1 -0
- package/src/global-contexts/HightideContext.ts +4 -1
- package/src/global-contexts/HightideProvider.tsx +22 -10
- package/src/global-contexts/hightide-config/HightideConfigUtils.ts +15 -12
- package/src/global-contexts/localization/LocalizationProvider.tsx +4 -2
- package/src/global-contexts/localization/forward-exports.ts +1 -0
- package/src/global-contexts/theme/ThemeContext.ts +11 -6
- package/src/global-contexts/theme/ThemeProvider.tsx +35 -38
- package/src/hooks/useMultiSelect.ts +1 -0
- package/src/hooks/useNativeKeyValueStore.ts +7 -1
- package/src/hooks/useSelect.ts +1 -0
- package/src/theme/resolvers/button.ts +38 -23
- package/src/theme/resolvers/chat.ts +418 -0
- package/src/theme/resolvers/checkbox.ts +26 -21
- package/src/theme/resolvers/chip.ts +32 -22
- package/src/theme/resolvers/coloring.ts +21 -17
- package/src/theme/resolvers/iconButton.ts +28 -18
- package/src/theme/resolvers/index.ts +2 -0
- package/src/theme/resolvers/input.ts +28 -19
- package/src/theme/resolvers/menu.ts +120 -0
- package/src/theme/resolvers/multiSelect.ts +39 -34
- package/src/theme/resolvers/select.ts +35 -30
- package/src/theme/themes/createTheme.ts +44 -15
- package/src/theme/themes/nativeThemes.ts +7 -6
- package/src/theme/types/color.ts +60 -0
- package/src/theme/types/components/button.ts +30 -0
- package/src/theme/types/components/chat.ts +198 -0
- package/src/theme/types/components/checkbox.ts +33 -0
- package/src/theme/types/components/chip.ts +30 -0
- package/src/theme/types/components/hightide.ts +42 -0
- package/src/theme/types/components/iconButton.ts +30 -0
- package/src/theme/types/components/index.ts +10 -0
- package/src/theme/types/components/input.ts +19 -0
- package/src/theme/types/components/menu.ts +57 -0
- package/src/theme/types/components/multiSelect.ts +47 -0
- package/src/theme/types/components/select.ts +47 -0
- package/src/theme/types/decoration.ts +11 -0
- package/src/theme/types/index.ts +4 -7
- package/src/theme/types/layout.ts +46 -0
- package/src/theme/types/resolver.ts +43 -1
- package/src/theme/types/theme.ts +14 -10
- package/src/theme/types/typography.ts +18 -0
- package/src/components/Button/Button.tsx +0 -101
- package/src/components/Button/index.ts +0 -1
- package/src/components/Checkbox/index.ts +0 -1
- package/src/components/IconButton/IconButton.tsx +0 -89
- package/src/components/IconButton/index.ts +0 -1
- package/src/components/Input/index.ts +0 -1
- package/src/components/MultiSelect/index.ts +0 -1
- package/src/components/Select/index.ts +0 -1
- package/src/icons/Icon.tsx +0 -35
- package/src/icons/index.ts +0 -1
- package/src/theme/types/button.ts +0 -41
- package/src/theme/types/checkbox.ts +0 -36
- package/src/theme/types/chip.ts +0 -41
- package/src/theme/types/components.ts +0 -17
- package/src/theme/types/iconButton.ts +0 -34
- package/src/theme/types/input.ts +0 -25
- package/src/theme/types/multiSelect.ts +0 -65
- package/src/theme/types/select.ts +0 -77
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @helpwave/hightide-native
|
|
2
|
+
|
|
3
|
+
React Native components, theme resolvers, and providers for helpwave apps.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @helpwave/hightide-native
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies (install in your app if not already present):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add react react-native react-native-svg @react-native-async-storage/async-storage
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
Wrap your app with `HightideProvider`:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { HightideProvider } from "@helpwave/hightide-native/global-contexts";
|
|
23
|
+
|
|
24
|
+
<HightideProvider theme={{ theme: "system" }} locale={{ locale: "system" }}>
|
|
25
|
+
{children}
|
|
26
|
+
</HightideProvider>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Import from the package subpaths (there is no root export):
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { Button, Input, Select } from "@helpwave/hightide-native/components";
|
|
35
|
+
import { HightideProvider } from "@helpwave/hightide-native/global-contexts";
|
|
36
|
+
import { useSelect } from "@helpwave/hightide-native/hooks";
|
|
37
|
+
import { Icon } from "@helpwave/hightide-native/icons";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Subpath | Contents |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `@helpwave/hightide-native/components` | UI components (`Button`, `Checkbox`, `Chip`, `Input`, …) |
|
|
43
|
+
| `@helpwave/hightide-native/global-contexts` | `HightideProvider` and related context providers |
|
|
44
|
+
| `@helpwave/hightide-native/hooks` | Native hooks (`useSelect`, `useMultiSelect`, …) |
|
|
45
|
+
| `@helpwave/hightide-native/icons` | Icon components |
|
|
46
|
+
| `@helpwave/hightide-native/theme` | Theme factories and style resolvers |
|
|
47
|
+
| `@helpwave/hightide-native/types` | Shared TypeScript types |
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
From the monorepo root:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm --filter @helpwave/hightide-native run init
|
|
55
|
+
pnpm --filter @helpwave/hightide-native run storybook
|
|
56
|
+
```
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"access": "public"
|
|
11
11
|
},
|
|
12
12
|
"license": "AGPL-3.0-only",
|
|
13
|
-
"version": "0.0.
|
|
13
|
+
"version": "0.0.5",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"files": [
|
|
16
16
|
"src"
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"lucide-react-native": "0.561.0",
|
|
46
|
-
"@helpwave/hightide-design": "0.0.
|
|
47
|
-
"@helpwave/hightide-utils": "0.0.
|
|
46
|
+
"@helpwave/hightide-design": "0.0.3",
|
|
47
|
+
"@helpwave/hightide-utils": "0.0.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@react-native-async-storage/async-storage": ">=2.0.0",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
type ReactNode
|
|
4
|
+
} from 'react'
|
|
5
|
+
import {
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
type ViewProps,
|
|
10
|
+
type ViewStyle
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
import {
|
|
13
|
+
Download,
|
|
14
|
+
FileText
|
|
15
|
+
} from 'lucide-react-native'
|
|
16
|
+
|
|
17
|
+
import { IconButton } from '@/src/components/user-interaction/IconButton'
|
|
18
|
+
import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
|
|
19
|
+
import type {
|
|
20
|
+
ChatAttachmentCardMetadataStyle,
|
|
21
|
+
ChatAttachmentCardNameStyle,
|
|
22
|
+
ChatAttachmentCardState,
|
|
23
|
+
ChatAttachmentCardStyle,
|
|
24
|
+
ChatMessageDirection
|
|
25
|
+
} from '@/src/theme/types/components/chat'
|
|
26
|
+
import type { StyleOverwrite } from '@/src/theme/types/resolver'
|
|
27
|
+
|
|
28
|
+
export type ChatAttachmentCardProps = Omit<ViewProps, 'style'> & {
|
|
29
|
+
name: ReactNode,
|
|
30
|
+
metadata?: ReactNode,
|
|
31
|
+
icon?: ReactNode,
|
|
32
|
+
direction?: ChatMessageDirection,
|
|
33
|
+
downloadLabel?: string,
|
|
34
|
+
onDownload?: () => void,
|
|
35
|
+
style?: StyleProp<ViewStyle>,
|
|
36
|
+
cardStyle?: StyleOverwrite<ChatAttachmentCardState, ChatAttachmentCardStyle>,
|
|
37
|
+
nameStyle?: StyleOverwrite<Record<string, never>, ChatAttachmentCardNameStyle>,
|
|
38
|
+
metadataStyle?: StyleOverwrite<Record<string, never>, ChatAttachmentCardMetadataStyle>,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ChatAttachmentCard = ({
|
|
42
|
+
name,
|
|
43
|
+
metadata,
|
|
44
|
+
icon,
|
|
45
|
+
direction = 'incoming',
|
|
46
|
+
downloadLabel = 'Download',
|
|
47
|
+
onDownload,
|
|
48
|
+
style,
|
|
49
|
+
cardStyle,
|
|
50
|
+
nameStyle,
|
|
51
|
+
metadataStyle,
|
|
52
|
+
...props
|
|
53
|
+
}: ChatAttachmentCardProps) => {
|
|
54
|
+
const { theme } = useTheme()
|
|
55
|
+
const state = useMemo(() => ({ direction }), [direction])
|
|
56
|
+
const staticState = useMemo(() => ({}), [])
|
|
57
|
+
|
|
58
|
+
const resolvedCardStyle = useMemo(
|
|
59
|
+
() => theme.components.chat.attachmentCard.container(state, cardStyle),
|
|
60
|
+
[theme, state, cardStyle]
|
|
61
|
+
)
|
|
62
|
+
const resolvedIconStyle = useMemo(
|
|
63
|
+
() => theme.components.chat.attachmentCard.icon(staticState),
|
|
64
|
+
[theme, staticState]
|
|
65
|
+
)
|
|
66
|
+
const resolvedIconColor = useMemo(
|
|
67
|
+
() => theme.components.chat.attachmentCard.iconColor(staticState),
|
|
68
|
+
[theme, staticState]
|
|
69
|
+
)
|
|
70
|
+
const resolvedNameStyle = useMemo(
|
|
71
|
+
() => theme.components.chat.attachmentCard.name(staticState, nameStyle),
|
|
72
|
+
[theme, staticState, nameStyle]
|
|
73
|
+
)
|
|
74
|
+
const resolvedMetadataStyle = useMemo(
|
|
75
|
+
() => theme.components.chat.attachmentCard.metadata(staticState, metadataStyle),
|
|
76
|
+
[theme, staticState, metadataStyle]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<View {...props} style={[resolvedCardStyle, style]}>
|
|
81
|
+
<View style={resolvedIconStyle}>
|
|
82
|
+
{icon ?? <FileText size={22} color={resolvedIconColor.color} />}
|
|
83
|
+
</View>
|
|
84
|
+
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
|
|
85
|
+
{typeof name === 'string' || typeof name === 'number' ? (
|
|
86
|
+
<Text style={resolvedNameStyle} numberOfLines={1}>{name}</Text>
|
|
87
|
+
) : (
|
|
88
|
+
name
|
|
89
|
+
)}
|
|
90
|
+
{metadata != null && (
|
|
91
|
+
typeof metadata === 'string' || typeof metadata === 'number' ? (
|
|
92
|
+
<Text style={resolvedMetadataStyle}>{metadata}</Text>
|
|
93
|
+
) : (
|
|
94
|
+
metadata
|
|
95
|
+
)
|
|
96
|
+
)}
|
|
97
|
+
</View>
|
|
98
|
+
{onDownload && (
|
|
99
|
+
<IconButton
|
|
100
|
+
accessibilityLabel={downloadLabel}
|
|
101
|
+
size="sm"
|
|
102
|
+
color="primary"
|
|
103
|
+
coloringStyle="text"
|
|
104
|
+
onPress={onDownload}
|
|
105
|
+
icon={Download}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
</View>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
type ReactNode
|
|
4
|
+
} from 'react'
|
|
5
|
+
import {
|
|
6
|
+
ScrollView,
|
|
7
|
+
View,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
type ViewProps,
|
|
10
|
+
type ViewStyle
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
|
|
13
|
+
import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
|
|
14
|
+
import type {
|
|
15
|
+
ChatConversationListFooterStyle,
|
|
16
|
+
ChatConversationListHeaderStyle,
|
|
17
|
+
ChatConversationListStyle
|
|
18
|
+
} from '@/src/theme/types/components/chat'
|
|
19
|
+
import type { StyleOverwrite } from '@/src/theme/types/resolver'
|
|
20
|
+
|
|
21
|
+
export type ChatConversationListProps = Omit<ViewProps, 'children' | 'style'> & {
|
|
22
|
+
header?: ReactNode,
|
|
23
|
+
footer?: ReactNode,
|
|
24
|
+
children?: ReactNode,
|
|
25
|
+
style?: StyleProp<ViewStyle>,
|
|
26
|
+
listStyle?: StyleOverwrite<Record<string, never>, ChatConversationListStyle>,
|
|
27
|
+
headerStyle?: StyleOverwrite<Record<string, never>, ChatConversationListHeaderStyle>,
|
|
28
|
+
contentStyle?: StyleProp<ViewStyle>,
|
|
29
|
+
footerStyle?: StyleOverwrite<Record<string, never>, ChatConversationListFooterStyle>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const ChatConversationList = ({
|
|
33
|
+
header,
|
|
34
|
+
footer,
|
|
35
|
+
children,
|
|
36
|
+
style,
|
|
37
|
+
listStyle,
|
|
38
|
+
headerStyle,
|
|
39
|
+
contentStyle,
|
|
40
|
+
footerStyle,
|
|
41
|
+
...props
|
|
42
|
+
}: ChatConversationListProps) => {
|
|
43
|
+
const { theme } = useTheme()
|
|
44
|
+
const state = useMemo(() => ({}), [])
|
|
45
|
+
|
|
46
|
+
const resolvedListStyle = useMemo(
|
|
47
|
+
() => theme.components.chat.conversationList.container(state, listStyle),
|
|
48
|
+
[theme, state, listStyle]
|
|
49
|
+
)
|
|
50
|
+
const resolvedHeaderStyle = useMemo(
|
|
51
|
+
() => theme.components.chat.conversationList.header(state, headerStyle),
|
|
52
|
+
[theme, state, headerStyle]
|
|
53
|
+
)
|
|
54
|
+
const resolvedFooterStyle = useMemo(
|
|
55
|
+
() => theme.components.chat.conversationList.footer(state, footerStyle),
|
|
56
|
+
[theme, state, footerStyle]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<View {...props} style={[resolvedListStyle, style]}>
|
|
61
|
+
{header != null && (
|
|
62
|
+
<View style={resolvedHeaderStyle}>{header}</View>
|
|
63
|
+
)}
|
|
64
|
+
<ScrollView style={{ flex: 1 }} contentContainerStyle={contentStyle}>
|
|
65
|
+
{children}
|
|
66
|
+
</ScrollView>
|
|
67
|
+
{footer != null && (
|
|
68
|
+
<View style={resolvedFooterStyle}>{footer}</View>
|
|
69
|
+
)}
|
|
70
|
+
</View>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Fragment,
|
|
3
|
+
useMemo,
|
|
4
|
+
type ReactNode
|
|
5
|
+
} from 'react'
|
|
6
|
+
import {
|
|
7
|
+
Pressable,
|
|
8
|
+
Text,
|
|
9
|
+
View,
|
|
10
|
+
type PressableProps,
|
|
11
|
+
type StyleProp,
|
|
12
|
+
type ViewStyle
|
|
13
|
+
} from 'react-native'
|
|
14
|
+
import {
|
|
15
|
+
Check,
|
|
16
|
+
CheckCheck
|
|
17
|
+
} from 'lucide-react-native'
|
|
18
|
+
|
|
19
|
+
import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
|
|
20
|
+
import type {
|
|
21
|
+
ChatConversationRowPreviewStyle,
|
|
22
|
+
ChatConversationRowState,
|
|
23
|
+
ChatConversationRowStyle,
|
|
24
|
+
ChatConversationRowTimestampStyle,
|
|
25
|
+
ChatConversationRowTitleStyle
|
|
26
|
+
} from '@/src/theme/types/components/chat'
|
|
27
|
+
import type { StyleOverwrite } from '@/src/theme/types/resolver'
|
|
28
|
+
|
|
29
|
+
export type ChatConversationSentIndicator = 'sent' | 'sentAndReceived'
|
|
30
|
+
|
|
31
|
+
export type ChatConversationRowProps = Omit<PressableProps, 'children' | 'style'> & {
|
|
32
|
+
avatar: ReactNode,
|
|
33
|
+
title: ReactNode,
|
|
34
|
+
timestamp?: ReactNode,
|
|
35
|
+
preview?: ReactNode,
|
|
36
|
+
unreadCount?: number,
|
|
37
|
+
isSelected?: boolean,
|
|
38
|
+
sentIndicator?: ChatConversationSentIndicator,
|
|
39
|
+
style?: StyleProp<ViewStyle>,
|
|
40
|
+
rowStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowStyle>,
|
|
41
|
+
titleStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowTitleStyle>,
|
|
42
|
+
timestampStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowTimestampStyle>,
|
|
43
|
+
previewStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowPreviewStyle>,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type PressableInteraction = {
|
|
47
|
+
pressed: boolean,
|
|
48
|
+
hovered?: boolean,
|
|
49
|
+
focused?: boolean,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const ChatConversationRow = ({
|
|
53
|
+
avatar,
|
|
54
|
+
title,
|
|
55
|
+
timestamp,
|
|
56
|
+
preview,
|
|
57
|
+
unreadCount,
|
|
58
|
+
isSelected = false,
|
|
59
|
+
sentIndicator,
|
|
60
|
+
disabled,
|
|
61
|
+
style,
|
|
62
|
+
rowStyle,
|
|
63
|
+
titleStyle,
|
|
64
|
+
timestampStyle,
|
|
65
|
+
previewStyle,
|
|
66
|
+
...props
|
|
67
|
+
}: ChatConversationRowProps) => {
|
|
68
|
+
const { theme } = useTheme()
|
|
69
|
+
const isUnread = (unreadCount ?? 0) > 0
|
|
70
|
+
const SentIndicatorIcon = sentIndicator === 'sentAndReceived' ? CheckCheck : Check
|
|
71
|
+
|
|
72
|
+
const resolveState = (interaction: PressableInteraction): ChatConversationRowState => ({
|
|
73
|
+
isUnread,
|
|
74
|
+
isSelected,
|
|
75
|
+
isDisabled: !!disabled,
|
|
76
|
+
isPressed: interaction.pressed,
|
|
77
|
+
isHovered: !!interaction.hovered,
|
|
78
|
+
isFocused: !!interaction.focused,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const staticState = useMemo(() => ({}), [])
|
|
82
|
+
const unreadBadge = useMemo(
|
|
83
|
+
() => theme.components.chat.conversationRow.unreadBadge(staticState),
|
|
84
|
+
[theme, staticState]
|
|
85
|
+
)
|
|
86
|
+
const unreadBadgeText = useMemo(
|
|
87
|
+
() => theme.components.chat.conversationRow.unreadBadgeText(staticState),
|
|
88
|
+
[theme, staticState]
|
|
89
|
+
)
|
|
90
|
+
const sentIndicatorColor = useMemo(
|
|
91
|
+
() => theme.components.chat.conversationRow.sentIndicator(staticState).color,
|
|
92
|
+
[theme, staticState]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Pressable
|
|
97
|
+
{...props}
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
style={(pressableState) => {
|
|
100
|
+
const state = resolveState(pressableState as PressableInteraction)
|
|
101
|
+
return [theme.components.chat.conversationRow.container(state, rowStyle), style]
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{(pressableState) => {
|
|
105
|
+
const state = resolveState(pressableState as PressableInteraction)
|
|
106
|
+
const resolvedTitle = theme.components.chat.conversationRow.title(state, titleStyle)
|
|
107
|
+
const resolvedTimestamp = theme.components.chat.conversationRow.timestamp(state, timestampStyle)
|
|
108
|
+
const resolvedPreview = theme.components.chat.conversationRow.preview(state, previewStyle)
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Fragment>
|
|
112
|
+
{avatar}
|
|
113
|
+
<View style={{ flex: 1, minWidth: 0, gap: 5 }}>
|
|
114
|
+
<View style={{ flexDirection: 'row', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
|
|
115
|
+
{typeof title === 'string' || typeof title === 'number' ? (
|
|
116
|
+
<Text style={resolvedTitle} numberOfLines={1}>{title}</Text>
|
|
117
|
+
) : (
|
|
118
|
+
title
|
|
119
|
+
)}
|
|
120
|
+
{timestamp != null && (
|
|
121
|
+
typeof timestamp === 'string' || typeof timestamp === 'number' ? (
|
|
122
|
+
<Text style={resolvedTimestamp}>{timestamp}</Text>
|
|
123
|
+
) : (
|
|
124
|
+
timestamp
|
|
125
|
+
)
|
|
126
|
+
)}
|
|
127
|
+
</View>
|
|
128
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
129
|
+
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center', gap: 4, minWidth: 0 }}>
|
|
130
|
+
{sentIndicator && (
|
|
131
|
+
<SentIndicatorIcon size={14} color={sentIndicatorColor} />
|
|
132
|
+
)}
|
|
133
|
+
{preview != null && (
|
|
134
|
+
typeof preview === 'string' || typeof preview === 'number' ? (
|
|
135
|
+
<Text style={resolvedPreview} numberOfLines={1}>{preview}</Text>
|
|
136
|
+
) : (
|
|
137
|
+
preview
|
|
138
|
+
)
|
|
139
|
+
)}
|
|
140
|
+
</View>
|
|
141
|
+
{isUnread && (
|
|
142
|
+
<View style={unreadBadge}>
|
|
143
|
+
<Text style={unreadBadgeText}>{unreadCount}</Text>
|
|
144
|
+
</View>
|
|
145
|
+
)}
|
|
146
|
+
</View>
|
|
147
|
+
</View>
|
|
148
|
+
</Fragment>
|
|
149
|
+
)
|
|
150
|
+
}}
|
|
151
|
+
</Pressable>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
type ReactNode
|
|
4
|
+
} from 'react'
|
|
5
|
+
import {
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
type ViewProps,
|
|
10
|
+
type ViewStyle
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
|
|
13
|
+
import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
|
|
14
|
+
import type {
|
|
15
|
+
ChatDateDividerStyle,
|
|
16
|
+
ChatDateDividerTextStyle
|
|
17
|
+
} from '@/src/theme/types/components/chat'
|
|
18
|
+
import type { StyleOverwrite } from '@/src/theme/types/resolver'
|
|
19
|
+
|
|
20
|
+
export type ChatDateDividerProps = Omit<ViewProps, 'children' | 'style'> & {
|
|
21
|
+
children?: ReactNode,
|
|
22
|
+
style?: StyleProp<ViewStyle>,
|
|
23
|
+
dividerStyle?: StyleOverwrite<Record<string, never>, ChatDateDividerStyle>,
|
|
24
|
+
textStyle?: StyleOverwrite<Record<string, never>, ChatDateDividerTextStyle>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ChatDateDivider = ({
|
|
28
|
+
children,
|
|
29
|
+
style,
|
|
30
|
+
dividerStyle,
|
|
31
|
+
textStyle,
|
|
32
|
+
...props
|
|
33
|
+
}: ChatDateDividerProps) => {
|
|
34
|
+
const { theme } = useTheme()
|
|
35
|
+
const state = useMemo(() => ({}), [])
|
|
36
|
+
|
|
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
|
+
)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View {...props} style={[resolvedDividerStyle, style]}>
|
|
48
|
+
{typeof children === 'string' || typeof children === 'number' ? (
|
|
49
|
+
<Text style={resolvedTextStyle}>{children}</Text>
|
|
50
|
+
) : (
|
|
51
|
+
children
|
|
52
|
+
)}
|
|
53
|
+
</View>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
type ReactNode
|
|
4
|
+
} from 'react'
|
|
5
|
+
import {
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
type ViewProps,
|
|
10
|
+
type ViewStyle
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
import { CheckCheck } from 'lucide-react-native'
|
|
13
|
+
|
|
14
|
+
import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
|
|
15
|
+
import type {
|
|
16
|
+
ChatMessageBubbleContainerStyle,
|
|
17
|
+
ChatMessageBubbleContentStyle,
|
|
18
|
+
ChatMessageBubbleState,
|
|
19
|
+
ChatMessageBubbleStyle,
|
|
20
|
+
ChatMessageBubbleTimestampStyle,
|
|
21
|
+
ChatMessageDirection
|
|
22
|
+
} from '@/src/theme/types/components/chat'
|
|
23
|
+
import type { StyleOverwrite } from '@/src/theme/types/resolver'
|
|
24
|
+
|
|
25
|
+
export type { ChatMessageDirection }
|
|
26
|
+
|
|
27
|
+
export type ChatMessageBubbleProps = Omit<ViewProps, 'children' | 'style'> & {
|
|
28
|
+
direction?: ChatMessageDirection,
|
|
29
|
+
timestamp?: ReactNode,
|
|
30
|
+
readReceipt?: ReactNode,
|
|
31
|
+
children?: ReactNode,
|
|
32
|
+
style?: StyleProp<ViewStyle>,
|
|
33
|
+
containerStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleContainerStyle>,
|
|
34
|
+
bubbleStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleStyle>,
|
|
35
|
+
contentStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleContentStyle>,
|
|
36
|
+
timestampStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleTimestampStyle>,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const ChatMessageBubble = ({
|
|
40
|
+
direction = 'incoming',
|
|
41
|
+
timestamp,
|
|
42
|
+
readReceipt,
|
|
43
|
+
children,
|
|
44
|
+
style,
|
|
45
|
+
containerStyle,
|
|
46
|
+
bubbleStyle,
|
|
47
|
+
contentStyle,
|
|
48
|
+
timestampStyle,
|
|
49
|
+
...props
|
|
50
|
+
}: ChatMessageBubbleProps) => {
|
|
51
|
+
const { theme } = useTheme()
|
|
52
|
+
const state = useMemo(() => ({ direction }), [direction])
|
|
53
|
+
const staticState = useMemo(() => ({}), [])
|
|
54
|
+
|
|
55
|
+
const resolvedContainerStyle = useMemo(
|
|
56
|
+
() => theme.components.chat.messageBubble.container(state, containerStyle),
|
|
57
|
+
[theme, state, containerStyle]
|
|
58
|
+
)
|
|
59
|
+
const resolvedBubbleStyle = useMemo(
|
|
60
|
+
() => theme.components.chat.messageBubble.bubble(state, bubbleStyle),
|
|
61
|
+
[theme, state, bubbleStyle]
|
|
62
|
+
)
|
|
63
|
+
const resolvedContentStyle = useMemo(
|
|
64
|
+
() => theme.components.chat.messageBubble.content(state, contentStyle),
|
|
65
|
+
[theme, state, contentStyle]
|
|
66
|
+
)
|
|
67
|
+
const resolvedTimestampStyle = useMemo(
|
|
68
|
+
() => theme.components.chat.messageBubble.timestamp(state, timestampStyle),
|
|
69
|
+
[theme, state, timestampStyle]
|
|
70
|
+
)
|
|
71
|
+
const resolvedReceiptStyle = useMemo(
|
|
72
|
+
() => theme.components.chat.messageBubble.receipt(staticState),
|
|
73
|
+
[theme, staticState]
|
|
74
|
+
)
|
|
75
|
+
const resolvedReceiptTextStyle = useMemo(
|
|
76
|
+
() => theme.components.chat.messageBubble.receiptText(staticState),
|
|
77
|
+
[theme, staticState]
|
|
78
|
+
)
|
|
79
|
+
const resolvedReceiptIcon = useMemo(
|
|
80
|
+
() => theme.components.chat.messageBubble.receiptIcon(staticState),
|
|
81
|
+
[theme, staticState]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View {...props} style={[resolvedContainerStyle, style]}>
|
|
86
|
+
<View style={resolvedBubbleStyle}>
|
|
87
|
+
{typeof children === 'string' || typeof children === 'number' ? (
|
|
88
|
+
<Text style={resolvedContentStyle}>{children}</Text>
|
|
89
|
+
) : (
|
|
90
|
+
children
|
|
91
|
+
)}
|
|
92
|
+
{timestamp != null && (
|
|
93
|
+
typeof timestamp === 'string' || typeof timestamp === 'number' ? (
|
|
94
|
+
<Text style={resolvedTimestampStyle}>{timestamp}</Text>
|
|
95
|
+
) : (
|
|
96
|
+
timestamp
|
|
97
|
+
)
|
|
98
|
+
)}
|
|
99
|
+
</View>
|
|
100
|
+
{readReceipt != null && (
|
|
101
|
+
<View style={resolvedReceiptStyle}>
|
|
102
|
+
<CheckCheck size={14} color={resolvedReceiptIcon.color} />
|
|
103
|
+
{typeof readReceipt === 'string' || typeof readReceipt === 'number' ? (
|
|
104
|
+
<Text style={resolvedReceiptTextStyle}>{readReceipt}</Text>
|
|
105
|
+
) : (
|
|
106
|
+
readReceipt
|
|
107
|
+
)}
|
|
108
|
+
</View>
|
|
109
|
+
)}
|
|
110
|
+
</View>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
type ReactNode
|
|
4
|
+
} from 'react'
|
|
5
|
+
import {
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type StyleProp,
|
|
9
|
+
type ViewProps,
|
|
10
|
+
type ViewStyle
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
|
|
13
|
+
import type { ColoringType } from '@helpwave/hightide-design/helpers'
|
|
14
|
+
|
|
15
|
+
import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
|
|
16
|
+
import type {
|
|
17
|
+
ChatMessageCardState,
|
|
18
|
+
ChatMessageCardStyle,
|
|
19
|
+
ChatMessageCardSubtitleStyle,
|
|
20
|
+
ChatMessageCardTitleStyle,
|
|
21
|
+
ChatMessageDirection
|
|
22
|
+
} from '@/src/theme/types/components/chat'
|
|
23
|
+
import type { StyleOverwrite } from '@/src/theme/types/resolver'
|
|
24
|
+
|
|
25
|
+
export type ChatMessageCardProps = Omit<ViewProps, 'children' | 'style'> & {
|
|
26
|
+
icon?: ReactNode,
|
|
27
|
+
title: ReactNode,
|
|
28
|
+
subtitle?: ReactNode,
|
|
29
|
+
badge?: ReactNode,
|
|
30
|
+
actions?: ReactNode,
|
|
31
|
+
color?: ColoringType,
|
|
32
|
+
direction?: ChatMessageDirection,
|
|
33
|
+
children?: ReactNode,
|
|
34
|
+
style?: StyleProp<ViewStyle>,
|
|
35
|
+
cardStyle?: StyleOverwrite<ChatMessageCardState, ChatMessageCardStyle>,
|
|
36
|
+
titleStyle?: StyleOverwrite<ChatMessageCardState, ChatMessageCardTitleStyle>,
|
|
37
|
+
subtitleStyle?: StyleOverwrite<Record<string, never>, ChatMessageCardSubtitleStyle>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const ChatMessageCard = ({
|
|
41
|
+
icon,
|
|
42
|
+
title,
|
|
43
|
+
subtitle,
|
|
44
|
+
badge,
|
|
45
|
+
actions,
|
|
46
|
+
color = 'primary',
|
|
47
|
+
direction = 'incoming',
|
|
48
|
+
children,
|
|
49
|
+
style,
|
|
50
|
+
cardStyle,
|
|
51
|
+
titleStyle,
|
|
52
|
+
subtitleStyle,
|
|
53
|
+
...props
|
|
54
|
+
}: ChatMessageCardProps) => {
|
|
55
|
+
const { theme } = useTheme()
|
|
56
|
+
const state = useMemo(() => ({ direction, color }), [direction, color])
|
|
57
|
+
const staticState = useMemo(() => ({}), [])
|
|
58
|
+
|
|
59
|
+
const resolvedCardStyle = useMemo(
|
|
60
|
+
() => theme.components.chat.messageCard.container(state, cardStyle),
|
|
61
|
+
[theme, state, cardStyle]
|
|
62
|
+
)
|
|
63
|
+
const resolvedHeaderStyle = useMemo(
|
|
64
|
+
() => theme.components.chat.messageCard.header(staticState),
|
|
65
|
+
[theme, staticState]
|
|
66
|
+
)
|
|
67
|
+
const resolvedIconStyle = useMemo(
|
|
68
|
+
() => theme.components.chat.messageCard.icon(state),
|
|
69
|
+
[theme, state]
|
|
70
|
+
)
|
|
71
|
+
const resolvedTitleStyle = useMemo(
|
|
72
|
+
() => theme.components.chat.messageCard.title(state, titleStyle),
|
|
73
|
+
[theme, state, titleStyle]
|
|
74
|
+
)
|
|
75
|
+
const resolvedSubtitleStyle = useMemo(
|
|
76
|
+
() => theme.components.chat.messageCard.subtitle(staticState, subtitleStyle),
|
|
77
|
+
[theme, staticState, subtitleStyle]
|
|
78
|
+
)
|
|
79
|
+
const resolvedBodyStyle = useMemo(
|
|
80
|
+
() => theme.components.chat.messageCard.body(staticState),
|
|
81
|
+
[theme, staticState]
|
|
82
|
+
)
|
|
83
|
+
const resolvedActionsStyle = useMemo(
|
|
84
|
+
() => theme.components.chat.messageCard.actions(staticState),
|
|
85
|
+
[theme, staticState]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<View {...props} style={[resolvedCardStyle, style]}>
|
|
90
|
+
<View style={resolvedHeaderStyle}>
|
|
91
|
+
{icon != null && (
|
|
92
|
+
<View style={resolvedIconStyle}>{icon}</View>
|
|
93
|
+
)}
|
|
94
|
+
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
|
|
95
|
+
{typeof title === 'string' || typeof title === 'number' ? (
|
|
96
|
+
<Text style={resolvedTitleStyle}>{title}</Text>
|
|
97
|
+
) : (
|
|
98
|
+
title
|
|
99
|
+
)}
|
|
100
|
+
{subtitle != null && (
|
|
101
|
+
typeof subtitle === 'string' || typeof subtitle === 'number' ? (
|
|
102
|
+
<Text style={resolvedSubtitleStyle}>{subtitle}</Text>
|
|
103
|
+
) : (
|
|
104
|
+
subtitle
|
|
105
|
+
)
|
|
106
|
+
)}
|
|
107
|
+
</View>
|
|
108
|
+
{badge}
|
|
109
|
+
</View>
|
|
110
|
+
{children != null && (
|
|
111
|
+
<View style={resolvedBodyStyle}>{children}</View>
|
|
112
|
+
)}
|
|
113
|
+
{actions != null && (
|
|
114
|
+
<View style={resolvedActionsStyle}>{actions}</View>
|
|
115
|
+
)}
|
|
116
|
+
</View>
|
|
117
|
+
)
|
|
118
|
+
}
|