@planningcenter/chat-react-native 3.11.0-rc.0 → 3.11.0-rc.2

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.
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource';
3
+ export type MetaProps = {
4
+ authorName: string;
5
+ createdAt: string;
6
+ };
7
+ export declare function ImageAttachmentLegacy({ attachment, metaProps, onMessageAttachmentLongPress, }: {
8
+ attachment: DenormalizedMessageAttachmentResource;
9
+ metaProps: MetaProps;
10
+ onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void;
11
+ }): React.JSX.Element;
12
+ //# sourceMappingURL=image_attachment_legacy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image_attachment_legacy.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation/attachments/image_attachment_legacy.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAA;AA2B7D,OAAO,EAAE,qCAAqC,EAAE,MAAM,2DAA2D,CAAA;AAMjH,MAAM,MAAM,SAAS,GAAG;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,wBAAgB,qBAAqB,CAAC,EACpC,UAAU,EACV,SAAS,EACT,4BAA4B,GAC7B,EAAE;IACD,UAAU,EAAE,qCAAqC,CAAA;IACjD,SAAS,EAAE,SAAS,CAAA;IACpB,4BAA4B,EAAE,CAAC,UAAU,EAAE,qCAAqC,KAAK,IAAI,CAAA;CAC1F,qBA8DA"}
@@ -0,0 +1,142 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { StyleSheet, Modal, useWindowDimensions, SafeAreaView, View, Linking, } from 'react-native';
3
+ import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler';
4
+ import { runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated';
5
+ import { tokens } from '../../../vendor/tapestry/tokens';
6
+ import { IconButton, Image, Heading, Text } from '../../display';
7
+ import colorFunction from 'color';
8
+ import { formatDatePreview } from '../../../utils/date';
9
+ import { PlatformPressable } from '@react-navigation/elements';
10
+ import { useTheme } from '../../../hooks';
11
+ const PAN_THRESHOLD_PX = 300;
12
+ export function ImageAttachmentLegacy({ attachment, metaProps, onMessageAttachmentLongPress, }) {
13
+ const { attributes } = attachment;
14
+ const { url, urlMedium, filename, metadata = {} } = attributes;
15
+ const { colors } = useTheme();
16
+ const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height });
17
+ const [visible, setVisible] = useState(false);
18
+ // shared values run on the native UI thread and prevents clogging up the JS thread
19
+ const dismissY = useSharedValue(0);
20
+ const opacity = useSharedValue(1);
21
+ const resetAnimations = useCallback(() => {
22
+ dismissY.value = withTiming(0);
23
+ opacity.value = withTiming(1);
24
+ }, [dismissY, opacity]);
25
+ const handleCloseModal = useCallback(() => {
26
+ setVisible(false);
27
+ resetAnimations();
28
+ }, [setVisible, resetAnimations]);
29
+ const panGesture = Gesture.Pan()
30
+ .onUpdate(e => {
31
+ dismissY.value = e.translationY;
32
+ opacity.value = 1 - Math.abs(e.translationY) / PAN_THRESHOLD_PX;
33
+ })
34
+ .onEnd(() => {
35
+ runOnJS(handleCloseModal)(); // Ensures we can call a JS function
36
+ });
37
+ const animatedImageStyle = useAnimatedStyle(() => ({
38
+ transform: [{ translateY: dismissY.value }],
39
+ opacity: opacity.value,
40
+ }));
41
+ return (<>
42
+ <PlatformPressable style={styles.container} onPress={() => setVisible(true)} onLongPress={() => onMessageAttachmentLongPress(attachment)} android_ripple={{ color: colors.androidRippleNeutral, foreground: true }} accessibilityHint="Long press for more options">
43
+ <Image source={{ uri: urlMedium || url }} style={styles.image} wrapperStyle={styles.imageWrapper} alt={filename}/>
44
+ </PlatformPressable>
45
+ <LightboxModal visible={visible} handleCloseModal={handleCloseModal} uri={urlMedium || url} metaProps={metaProps} panGesture={panGesture} animatedImageStyle={animatedImageStyle}/>
46
+ </>);
47
+ }
48
+ const LightboxModal = ({ uri, visible, handleCloseModal, metaProps, panGesture, animatedImageStyle, }) => {
49
+ const styles = useStyles();
50
+ const { authorName, createdAt } = metaProps;
51
+ const handleOpenInBrowser = () => {
52
+ Linking.openURL(uri);
53
+ };
54
+ return (<Modal visible={visible} transparent animationType="fade" onRequestClose={handleCloseModal}>
55
+ <SafeAreaView style={styles.modal}>
56
+ <GestureHandlerRootView>
57
+ <GestureDetector gesture={panGesture}>
58
+ <Image source={{ uri }} loadingBackgroundStyles={styles.lightboxImageLoading} style={styles.lightboxImage} animatedImageStyle={animatedImageStyle} resizeMode="contain" animated={true} alt=""/>
59
+ </GestureDetector>
60
+ <View style={styles.actionToolbar} accessibilityRole="toolbar">
61
+ <View style={styles.actionToolbarTextMeta}>
62
+ <Heading variant="h3" style={styles.actionToolbarTitle} numberOfLines={1}>
63
+ {authorName}
64
+ </Heading>
65
+ <Text variant="tertiary" style={styles.actionToolbarSubtitle}>
66
+ {formatDatePreview(createdAt)}
67
+ </Text>
68
+ </View>
69
+ <IconButton onPress={handleOpenInBrowser} name="general.newWindow" accessibilityRole="link" accessibilityLabel="Open image in browser" accessibilityHint="Image can be downloaded and shared through the browser." style={styles.actionButton} iconStyle={styles.actionButtonIcon} size="lg"/>
70
+ <IconButton onPress={handleCloseModal} name="general.x" accessibilityLabel="Close image" style={styles.actionButton} iconStyle={styles.actionButtonIcon}/>
71
+ </View>
72
+ </GestureHandlerRootView>
73
+ </SafeAreaView>
74
+ </Modal>);
75
+ };
76
+ const useStyles = ({ imageWidth = 100, imageHeight = 100 } = {}) => {
77
+ const { width: windowWidth } = useWindowDimensions();
78
+ const backgroundColor = tokens.colorNeutral7;
79
+ const transparentBackgroundColor = useMemo(() => colorFunction(backgroundColor).alpha(0.8).toString(), [backgroundColor]);
80
+ return StyleSheet.create({
81
+ container: {
82
+ maxWidth: '100%',
83
+ },
84
+ imageWrapper: {
85
+ width: '100%',
86
+ minWidth: 200,
87
+ aspectRatio: imageWidth / imageHeight,
88
+ },
89
+ image: {
90
+ borderRadius: 8,
91
+ },
92
+ modal: {
93
+ flex: 1,
94
+ backgroundColor,
95
+ justifyContent: 'center',
96
+ alignItems: 'center',
97
+ },
98
+ lightboxImage: {
99
+ height: '100%',
100
+ width: windowWidth,
101
+ backgroundColor,
102
+ },
103
+ lightboxImageLoading: {
104
+ backgroundColor,
105
+ },
106
+ actionToolbar: {
107
+ width: '100%',
108
+ position: 'absolute',
109
+ top: 0,
110
+ flexDirection: 'row',
111
+ alignItems: 'center',
112
+ gap: 20,
113
+ paddingHorizontal: 16,
114
+ paddingTop: 16,
115
+ paddingBottom: 8,
116
+ backgroundColor: transparentBackgroundColor,
117
+ },
118
+ actionToolbarTextMeta: {
119
+ flex: 1,
120
+ },
121
+ actionToolbarTitle: {
122
+ marginRight: 'auto',
123
+ flexShrink: 1,
124
+ color: tokens.colorNeutral88,
125
+ },
126
+ actionToolbarSubtitle: {
127
+ color: tokens.colorNeutral68,
128
+ },
129
+ actionButton: {
130
+ backgroundColor,
131
+ height: 40,
132
+ width: 40,
133
+ borderRadius: 50,
134
+ borderWidth: 1,
135
+ borderColor: tokens.colorNeutral24,
136
+ },
137
+ actionButtonIcon: {
138
+ color: tokens.colorNeutral88,
139
+ },
140
+ });
141
+ };
142
+ //# sourceMappingURL=image_attachment_legacy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image_attachment_legacy.js","sourceRoot":"","sources":["../../../../src/components/conversation/attachments/image_attachment_legacy.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useCallback, useMemo, useState } from 'react'\nimport {\n StyleSheet,\n Modal,\n useWindowDimensions,\n SafeAreaView,\n View,\n Linking,\n ImageStyle,\n} from 'react-native'\nimport {\n Gesture,\n GestureDetector,\n GestureHandlerRootView,\n type PanGesture,\n} from 'react-native-gesture-handler'\nimport {\n runOnJS,\n useAnimatedStyle,\n useSharedValue,\n withTiming,\n type AnimatedStyle,\n} from 'react-native-reanimated'\nimport { tokens } from '../../../vendor/tapestry/tokens'\nimport { IconButton, Image, Heading, Text } from '../../display'\nimport colorFunction from 'color'\nimport { formatDatePreview } from '../../../utils/date'\nimport { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'\nimport { PlatformPressable } from '@react-navigation/elements'\nimport { useTheme } from '../../../hooks'\n\nconst PAN_THRESHOLD_PX = 300\n\nexport type MetaProps = {\n authorName: string\n createdAt: string\n}\n\nexport function ImageAttachmentLegacy({\n attachment,\n metaProps,\n onMessageAttachmentLongPress,\n}: {\n attachment: DenormalizedMessageAttachmentResource\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n}) {\n const { attributes } = attachment\n const { url, urlMedium, filename, metadata = {} } = attributes\n const { colors } = useTheme()\n\n const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height })\n const [visible, setVisible] = useState(false)\n\n // shared values run on the native UI thread and prevents clogging up the JS thread\n const dismissY = useSharedValue(0)\n const opacity = useSharedValue(1)\n\n const resetAnimations = useCallback(() => {\n dismissY.value = withTiming(0)\n opacity.value = withTiming(1)\n }, [dismissY, opacity])\n\n const handleCloseModal = useCallback(() => {\n setVisible(false)\n resetAnimations()\n }, [setVisible, resetAnimations])\n\n const panGesture = Gesture.Pan()\n .onUpdate(e => {\n dismissY.value = e.translationY\n opacity.value = 1 - Math.abs(e.translationY) / PAN_THRESHOLD_PX\n })\n .onEnd(() => {\n runOnJS(handleCloseModal)() // Ensures we can call a JS function\n })\n\n const animatedImageStyle = useAnimatedStyle(() => ({\n transform: [{ translateY: dismissY.value }],\n opacity: opacity.value,\n }))\n\n return (\n <>\n <PlatformPressable\n style={styles.container}\n onPress={() => setVisible(true)}\n onLongPress={() => onMessageAttachmentLongPress(attachment)}\n android_ripple={{ color: colors.androidRippleNeutral, foreground: true }}\n accessibilityHint=\"Long press for more options\"\n >\n <Image\n source={{ uri: urlMedium || url }}\n style={styles.image}\n wrapperStyle={styles.imageWrapper}\n alt={filename}\n />\n </PlatformPressable>\n <LightboxModal\n visible={visible}\n handleCloseModal={handleCloseModal}\n uri={urlMedium || url}\n metaProps={metaProps}\n panGesture={panGesture}\n animatedImageStyle={animatedImageStyle}\n />\n </>\n )\n}\n\ninterface LightboxModalProps {\n visible: boolean\n handleCloseModal: () => void\n uri: string\n metaProps: MetaProps\n panGesture: PanGesture\n animatedImageStyle: AnimatedStyle<ImageStyle>\n}\n\nconst LightboxModal = ({\n uri,\n visible,\n handleCloseModal,\n metaProps,\n panGesture,\n animatedImageStyle,\n}: LightboxModalProps) => {\n const styles = useStyles()\n\n const { authorName, createdAt } = metaProps\n\n const handleOpenInBrowser = () => {\n Linking.openURL(uri)\n }\n\n return (\n <Modal visible={visible} transparent animationType=\"fade\" onRequestClose={handleCloseModal}>\n <SafeAreaView style={styles.modal}>\n <GestureHandlerRootView>\n <GestureDetector gesture={panGesture}>\n <Image\n source={{ uri }}\n loadingBackgroundStyles={styles.lightboxImageLoading}\n style={styles.lightboxImage}\n animatedImageStyle={animatedImageStyle}\n resizeMode=\"contain\"\n animated={true}\n alt=\"\"\n />\n </GestureDetector>\n <View style={styles.actionToolbar} accessibilityRole=\"toolbar\">\n <View style={styles.actionToolbarTextMeta}>\n <Heading variant=\"h3\" style={styles.actionToolbarTitle} numberOfLines={1}>\n {authorName}\n </Heading>\n <Text variant=\"tertiary\" style={styles.actionToolbarSubtitle}>\n {formatDatePreview(createdAt)}\n </Text>\n </View>\n <IconButton\n onPress={handleOpenInBrowser}\n name=\"general.newWindow\"\n accessibilityRole=\"link\"\n accessibilityLabel=\"Open image in browser\"\n accessibilityHint=\"Image can be downloaded and shared through the browser.\"\n style={styles.actionButton}\n iconStyle={styles.actionButtonIcon}\n size=\"lg\"\n />\n <IconButton\n onPress={handleCloseModal}\n name=\"general.x\"\n accessibilityLabel=\"Close image\"\n style={styles.actionButton}\n iconStyle={styles.actionButtonIcon}\n />\n </View>\n </GestureHandlerRootView>\n </SafeAreaView>\n </Modal>\n )\n}\n\ninterface UseStylesProps {\n imageWidth?: number\n imageHeight?: number\n}\n\nconst useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {}) => {\n const { width: windowWidth } = useWindowDimensions()\n const backgroundColor = tokens.colorNeutral7\n const transparentBackgroundColor = useMemo(\n () => colorFunction(backgroundColor).alpha(0.8).toString(),\n [backgroundColor]\n )\n\n return StyleSheet.create({\n container: {\n maxWidth: '100%',\n },\n imageWrapper: {\n width: '100%',\n minWidth: 200,\n aspectRatio: imageWidth / imageHeight,\n },\n image: {\n borderRadius: 8,\n },\n modal: {\n flex: 1,\n backgroundColor,\n justifyContent: 'center',\n alignItems: 'center',\n },\n lightboxImage: {\n height: '100%',\n width: windowWidth,\n backgroundColor,\n },\n lightboxImageLoading: {\n backgroundColor,\n },\n actionToolbar: {\n width: '100%',\n position: 'absolute',\n top: 0,\n flexDirection: 'row',\n alignItems: 'center',\n gap: 20,\n paddingHorizontal: 16,\n paddingTop: 16,\n paddingBottom: 8,\n backgroundColor: transparentBackgroundColor,\n },\n actionToolbarTextMeta: {\n flex: 1,\n },\n actionToolbarTitle: {\n marginRight: 'auto',\n flexShrink: 1,\n color: tokens.colorNeutral88,\n },\n actionToolbarSubtitle: {\n color: tokens.colorNeutral68,\n },\n actionButton: {\n backgroundColor,\n height: 40,\n width: 40,\n borderRadius: 50,\n borderWidth: 1,\n borderColor: tokens.colorNeutral24,\n },\n actionButtonIcon: {\n color: tokens.colorNeutral88,\n },\n })\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { DenormalizedAttachmentResource, DenormalizedMessageAttachmentResource } from '../../types/resources/denormalized_attachment_resource';
3
- import { type MetaProps } from './attachments/image_attachment';
3
+ import { type MetaProps } from './attachments/image_attachment_legacy';
4
4
  export declare function MessageAttachments(props: {
5
5
  attachments: DenormalizedAttachmentResource[];
6
6
  metaProps: MetaProps;
@@ -1 +1 @@
1
- {"version":3,"file":"message_attachments.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/message_attachments.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EACL,8BAA8B,EAC9B,qCAAqC,EACtC,MAAM,wDAAwD,CAAA;AAM/D,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,gCAAgC,CAAA;AAEhF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,WAAW,EAAE,8BAA8B,EAAE,CAAA;IAC7C,SAAS,EAAE,SAAS,CAAA;IACpB,4BAA4B,EAAE,CAAC,UAAU,EAAE,qCAAqC,KAAK,IAAI,CAAA;IACzF,kBAAkB,EAAE,MAAM,IAAI,CAAA;CAC/B,4BAuCA"}
1
+ {"version":3,"file":"message_attachments.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/message_attachments.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EACL,8BAA8B,EAC9B,qCAAqC,EACtC,MAAM,wDAAwD,CAAA;AAM/D,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,uCAAuC,CAAA;AAM7F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,WAAW,EAAE,8BAA8B,EAAE,CAAA;IAC7C,SAAS,EAAE,SAAS,CAAA;IACpB,4BAA4B,EAAE,CAAC,UAAU,EAAE,qCAAqC,KAAK,IAAI,CAAA;IACzF,kBAAkB,EAAE,MAAM,IAAI,CAAA;CAC/B,4BA2DA"}
@@ -5,21 +5,30 @@ import { VideoAttachment } from './attachments/video_attachment';
5
5
  import { GiphyAttachment } from './attachments/giphy_attachment';
6
6
  import { GenericFileAttachment } from './attachments/generic_file_attachment';
7
7
  import { ExpandedLink } from './attachments/expanded_link';
8
+ import { ImageAttachmentLegacy } from './attachments/image_attachment_legacy';
8
9
  import { ImageAttachment } from './attachments/image_attachment';
10
+ // Temporarily controls whether image attachments can be opened in a gallery lightbox. (Will remove after QA approves project.)
11
+ const ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY = false;
9
12
  export function MessageAttachments(props) {
10
13
  const styles = useStyles();
11
14
  const { attachments, metaProps, onMessageAttachmentLongPress, onMessageLongPress } = props;
12
15
  if (!attachments || attachments.length === 0)
13
16
  return null;
17
+ const imageAttachments = attachments.filter(attachment => attachment.type === 'MessageAttachment' &&
18
+ attachment.attributes?.contentType?.startsWith('image/'));
19
+ const showImageAttachmentGroup = ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY && imageAttachments.length > 0;
14
20
  return (<View style={styles.attachmentsContainer}>
15
- {attachments.map(attachment => {
21
+ {showImageAttachmentGroup &&
22
+ imageAttachments.map((image, index) => (<ImageAttachment key={`${image.id}-${index}`} attachment={image} metaProps={metaProps} onMessageAttachmentLongPress={onMessageAttachmentLongPress}/>))}
23
+
24
+ {attachments.map((attachment, index) => {
16
25
  switch (attachment.type) {
17
26
  case 'MessageAttachment':
18
- return (<MessageAttachment key={attachment.id} attachment={attachment} metaProps={metaProps} onMessageAttachmentLongPress={onMessageAttachmentLongPress}/>);
27
+ return (<MessageAttachment key={`${attachment.id}-${index}`} attachment={attachment} metaProps={metaProps} onMessageAttachmentLongPress={onMessageAttachmentLongPress}/>);
19
28
  case 'giphy':
20
- return (<GiphyAttachment key={attachment.id || attachment.titleLink} attachment={attachment} onMessageLongPress={onMessageLongPress}/>);
29
+ return (<GiphyAttachment key={`${attachment.id || attachment.titleLink}-${index}`} attachment={attachment} onMessageLongPress={onMessageLongPress}/>);
21
30
  case 'ExpandedLink':
22
- return (<ExpandedLink key={attachment.id} attachment={attachment} onMessageLongPress={onMessageLongPress}/>);
31
+ return (<ExpandedLink key={`${attachment.id}-${index}`} attachment={attachment} onMessageLongPress={onMessageLongPress}/>);
23
32
  default:
24
33
  return null;
25
34
  }
@@ -30,9 +39,12 @@ function MessageAttachment({ attachment, metaProps, onMessageAttachmentLongPress
30
39
  const { attributes } = attachment;
31
40
  const contentType = attributes?.contentType;
32
41
  const basicType = contentType ? contentType.split('/')[0] : '';
42
+ if (basicType === 'image' && ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY) {
43
+ return null;
44
+ }
33
45
  switch (basicType) {
34
46
  case 'image':
35
- return (<ImageAttachment attachment={attachment} metaProps={metaProps} onMessageAttachmentLongPress={onMessageAttachmentLongPress}/>);
47
+ return (<ImageAttachmentLegacy attachment={attachment} metaProps={metaProps} onMessageAttachmentLongPress={onMessageAttachmentLongPress}/>);
36
48
  case 'video':
37
49
  return (<VideoAttachment attachment={attachment} onMessageAttachmentLongPress={onMessageAttachmentLongPress}/>);
38
50
  case 'audio':
@@ -1 +1 @@
1
- {"version":3,"file":"message_attachments.js","sourceRoot":"","sources":["../../../src/components/conversation/message_attachments.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react'\nimport { View, StyleSheet } from 'react-native'\nimport {\n DenormalizedAttachmentResource,\n DenormalizedMessageAttachmentResource,\n} from '../../types/resources/denormalized_attachment_resource'\nimport { AudioAttachment } from './attachments/audio_attachment'\nimport { VideoAttachment } from './attachments/video_attachment'\nimport { GiphyAttachment } from './attachments/giphy_attachment'\nimport { GenericFileAttachment } from './attachments/generic_file_attachment'\nimport { ExpandedLink } from './attachments/expanded_link'\nimport { ImageAttachment, type MetaProps } from './attachments/image_attachment'\n\nexport function MessageAttachments(props: {\n attachments: DenormalizedAttachmentResource[]\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n onMessageLongPress: () => void\n}) {\n const styles = useStyles()\n const { attachments, metaProps, onMessageAttachmentLongPress, onMessageLongPress } = props\n if (!attachments || attachments.length === 0) return null\n return (\n <View style={styles.attachmentsContainer}>\n {attachments.map(attachment => {\n switch (attachment.type) {\n case 'MessageAttachment':\n return (\n <MessageAttachment\n key={attachment.id}\n attachment={attachment}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'giphy':\n return (\n <GiphyAttachment\n key={attachment.id || attachment.titleLink}\n attachment={attachment}\n onMessageLongPress={onMessageLongPress}\n />\n )\n case 'ExpandedLink':\n return (\n <ExpandedLink\n key={attachment.id}\n attachment={attachment}\n onMessageLongPress={onMessageLongPress}\n />\n )\n default:\n return null\n }\n })}\n </View>\n )\n}\n\nfunction MessageAttachment({\n attachment,\n metaProps,\n onMessageAttachmentLongPress,\n}: {\n attachment: DenormalizedMessageAttachmentResource\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n}) {\n const { attributes } = attachment\n const contentType = attributes?.contentType\n const basicType = contentType ? contentType.split('/')[0] : ''\n switch (basicType) {\n case 'image':\n return (\n <ImageAttachment\n attachment={attachment}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'video':\n return (\n <VideoAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'audio':\n return (\n <AudioAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n default:\n return (\n <GenericFileAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n }\n}\n\nconst useStyles = () => {\n return StyleSheet.create({\n attachmentsContainer: {\n gap: 2,\n padding: 2,\n },\n })\n}\n"]}
1
+ {"version":3,"file":"message_attachments.js","sourceRoot":"","sources":["../../../src/components/conversation/message_attachments.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react'\nimport { View, StyleSheet } from 'react-native'\nimport {\n DenormalizedAttachmentResource,\n DenormalizedMessageAttachmentResource,\n} from '../../types/resources/denormalized_attachment_resource'\nimport { AudioAttachment } from './attachments/audio_attachment'\nimport { VideoAttachment } from './attachments/video_attachment'\nimport { GiphyAttachment } from './attachments/giphy_attachment'\nimport { GenericFileAttachment } from './attachments/generic_file_attachment'\nimport { ExpandedLink } from './attachments/expanded_link'\nimport { ImageAttachmentLegacy, type MetaProps } from './attachments/image_attachment_legacy'\nimport { ImageAttachment } from './attachments/image_attachment'\n\n// Temporarily controls whether image attachments can be opened in a gallery lightbox. (Will remove after QA approves project.)\nconst ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY = false\n\nexport function MessageAttachments(props: {\n attachments: DenormalizedAttachmentResource[]\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n onMessageLongPress: () => void\n}) {\n const styles = useStyles()\n const { attachments, metaProps, onMessageAttachmentLongPress, onMessageLongPress } = props\n if (!attachments || attachments.length === 0) return null\n\n const imageAttachments = attachments.filter(\n attachment =>\n attachment.type === 'MessageAttachment' &&\n attachment.attributes?.contentType?.startsWith('image/')\n ) as DenormalizedMessageAttachmentResource[]\n\n const showImageAttachmentGroup =\n ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY && imageAttachments.length > 0\n\n return (\n <View style={styles.attachmentsContainer}>\n {showImageAttachmentGroup &&\n imageAttachments.map((image, index) => (\n <ImageAttachment\n key={`${image.id}-${index}`}\n attachment={image}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n ))}\n\n {attachments.map((attachment, index) => {\n switch (attachment.type) {\n case 'MessageAttachment':\n return (\n <MessageAttachment\n key={`${attachment.id}-${index}`}\n attachment={attachment}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'giphy':\n return (\n <GiphyAttachment\n key={`${attachment.id || attachment.titleLink}-${index}`}\n attachment={attachment}\n onMessageLongPress={onMessageLongPress}\n />\n )\n case 'ExpandedLink':\n return (\n <ExpandedLink\n key={`${attachment.id}-${index}`}\n attachment={attachment}\n onMessageLongPress={onMessageLongPress}\n />\n )\n default:\n return null\n }\n })}\n </View>\n )\n}\n\nfunction MessageAttachment({\n attachment,\n metaProps,\n onMessageAttachmentLongPress,\n}: {\n attachment: DenormalizedMessageAttachmentResource\n metaProps: MetaProps\n onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void\n}) {\n const { attributes } = attachment\n const contentType = attributes?.contentType\n const basicType = contentType ? contentType.split('/')[0] : ''\n\n if (basicType === 'image' && ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY) {\n return null\n }\n\n switch (basicType) {\n case 'image':\n return (\n <ImageAttachmentLegacy\n attachment={attachment}\n metaProps={metaProps}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'video':\n return (\n <VideoAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n case 'audio':\n return (\n <AudioAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n default:\n return (\n <GenericFileAttachment\n attachment={attachment}\n onMessageAttachmentLongPress={onMessageAttachmentLongPress}\n />\n )\n }\n}\n\nconst useStyles = () => {\n return StyleSheet.create({\n attachmentsContainer: {\n gap: 2,\n padding: 2,\n },\n })\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.11.0-rc.0",
3
+ "version": "3.11.0-rc.2",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -55,5 +55,5 @@
55
55
  "prettier": "^3.4.2",
56
56
  "typescript": "<5.6.0"
57
57
  },
58
- "gitHead": "91a3576e8f767295ff93a8be630d99459da9f676"
58
+ "gitHead": "e3035cb2dfb6434b91ecc562f115158a5c2ed051"
59
59
  }
@@ -0,0 +1,258 @@
1
+ import React, { useCallback, useMemo, useState } from 'react'
2
+ import {
3
+ StyleSheet,
4
+ Modal,
5
+ useWindowDimensions,
6
+ SafeAreaView,
7
+ View,
8
+ Linking,
9
+ ImageStyle,
10
+ } from 'react-native'
11
+ import {
12
+ Gesture,
13
+ GestureDetector,
14
+ GestureHandlerRootView,
15
+ type PanGesture,
16
+ } from 'react-native-gesture-handler'
17
+ import {
18
+ runOnJS,
19
+ useAnimatedStyle,
20
+ useSharedValue,
21
+ withTiming,
22
+ type AnimatedStyle,
23
+ } from 'react-native-reanimated'
24
+ import { tokens } from '../../../vendor/tapestry/tokens'
25
+ import { IconButton, Image, Heading, Text } from '../../display'
26
+ import colorFunction from 'color'
27
+ import { formatDatePreview } from '../../../utils/date'
28
+ import { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
29
+ import { PlatformPressable } from '@react-navigation/elements'
30
+ import { useTheme } from '../../../hooks'
31
+
32
+ const PAN_THRESHOLD_PX = 300
33
+
34
+ export type MetaProps = {
35
+ authorName: string
36
+ createdAt: string
37
+ }
38
+
39
+ export function ImageAttachmentLegacy({
40
+ attachment,
41
+ metaProps,
42
+ onMessageAttachmentLongPress,
43
+ }: {
44
+ attachment: DenormalizedMessageAttachmentResource
45
+ metaProps: MetaProps
46
+ onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void
47
+ }) {
48
+ const { attributes } = attachment
49
+ const { url, urlMedium, filename, metadata = {} } = attributes
50
+ const { colors } = useTheme()
51
+
52
+ const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height })
53
+ const [visible, setVisible] = useState(false)
54
+
55
+ // shared values run on the native UI thread and prevents clogging up the JS thread
56
+ const dismissY = useSharedValue(0)
57
+ const opacity = useSharedValue(1)
58
+
59
+ const resetAnimations = useCallback(() => {
60
+ dismissY.value = withTiming(0)
61
+ opacity.value = withTiming(1)
62
+ }, [dismissY, opacity])
63
+
64
+ const handleCloseModal = useCallback(() => {
65
+ setVisible(false)
66
+ resetAnimations()
67
+ }, [setVisible, resetAnimations])
68
+
69
+ const panGesture = Gesture.Pan()
70
+ .onUpdate(e => {
71
+ dismissY.value = e.translationY
72
+ opacity.value = 1 - Math.abs(e.translationY) / PAN_THRESHOLD_PX
73
+ })
74
+ .onEnd(() => {
75
+ runOnJS(handleCloseModal)() // Ensures we can call a JS function
76
+ })
77
+
78
+ const animatedImageStyle = useAnimatedStyle(() => ({
79
+ transform: [{ translateY: dismissY.value }],
80
+ opacity: opacity.value,
81
+ }))
82
+
83
+ return (
84
+ <>
85
+ <PlatformPressable
86
+ style={styles.container}
87
+ onPress={() => setVisible(true)}
88
+ onLongPress={() => onMessageAttachmentLongPress(attachment)}
89
+ android_ripple={{ color: colors.androidRippleNeutral, foreground: true }}
90
+ accessibilityHint="Long press for more options"
91
+ >
92
+ <Image
93
+ source={{ uri: urlMedium || url }}
94
+ style={styles.image}
95
+ wrapperStyle={styles.imageWrapper}
96
+ alt={filename}
97
+ />
98
+ </PlatformPressable>
99
+ <LightboxModal
100
+ visible={visible}
101
+ handleCloseModal={handleCloseModal}
102
+ uri={urlMedium || url}
103
+ metaProps={metaProps}
104
+ panGesture={panGesture}
105
+ animatedImageStyle={animatedImageStyle}
106
+ />
107
+ </>
108
+ )
109
+ }
110
+
111
+ interface LightboxModalProps {
112
+ visible: boolean
113
+ handleCloseModal: () => void
114
+ uri: string
115
+ metaProps: MetaProps
116
+ panGesture: PanGesture
117
+ animatedImageStyle: AnimatedStyle<ImageStyle>
118
+ }
119
+
120
+ const LightboxModal = ({
121
+ uri,
122
+ visible,
123
+ handleCloseModal,
124
+ metaProps,
125
+ panGesture,
126
+ animatedImageStyle,
127
+ }: LightboxModalProps) => {
128
+ const styles = useStyles()
129
+
130
+ const { authorName, createdAt } = metaProps
131
+
132
+ const handleOpenInBrowser = () => {
133
+ Linking.openURL(uri)
134
+ }
135
+
136
+ return (
137
+ <Modal visible={visible} transparent animationType="fade" onRequestClose={handleCloseModal}>
138
+ <SafeAreaView style={styles.modal}>
139
+ <GestureHandlerRootView>
140
+ <GestureDetector gesture={panGesture}>
141
+ <Image
142
+ source={{ uri }}
143
+ loadingBackgroundStyles={styles.lightboxImageLoading}
144
+ style={styles.lightboxImage}
145
+ animatedImageStyle={animatedImageStyle}
146
+ resizeMode="contain"
147
+ animated={true}
148
+ alt=""
149
+ />
150
+ </GestureDetector>
151
+ <View style={styles.actionToolbar} accessibilityRole="toolbar">
152
+ <View style={styles.actionToolbarTextMeta}>
153
+ <Heading variant="h3" style={styles.actionToolbarTitle} numberOfLines={1}>
154
+ {authorName}
155
+ </Heading>
156
+ <Text variant="tertiary" style={styles.actionToolbarSubtitle}>
157
+ {formatDatePreview(createdAt)}
158
+ </Text>
159
+ </View>
160
+ <IconButton
161
+ onPress={handleOpenInBrowser}
162
+ name="general.newWindow"
163
+ accessibilityRole="link"
164
+ accessibilityLabel="Open image in browser"
165
+ accessibilityHint="Image can be downloaded and shared through the browser."
166
+ style={styles.actionButton}
167
+ iconStyle={styles.actionButtonIcon}
168
+ size="lg"
169
+ />
170
+ <IconButton
171
+ onPress={handleCloseModal}
172
+ name="general.x"
173
+ accessibilityLabel="Close image"
174
+ style={styles.actionButton}
175
+ iconStyle={styles.actionButtonIcon}
176
+ />
177
+ </View>
178
+ </GestureHandlerRootView>
179
+ </SafeAreaView>
180
+ </Modal>
181
+ )
182
+ }
183
+
184
+ interface UseStylesProps {
185
+ imageWidth?: number
186
+ imageHeight?: number
187
+ }
188
+
189
+ const useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {}) => {
190
+ const { width: windowWidth } = useWindowDimensions()
191
+ const backgroundColor = tokens.colorNeutral7
192
+ const transparentBackgroundColor = useMemo(
193
+ () => colorFunction(backgroundColor).alpha(0.8).toString(),
194
+ [backgroundColor]
195
+ )
196
+
197
+ return StyleSheet.create({
198
+ container: {
199
+ maxWidth: '100%',
200
+ },
201
+ imageWrapper: {
202
+ width: '100%',
203
+ minWidth: 200,
204
+ aspectRatio: imageWidth / imageHeight,
205
+ },
206
+ image: {
207
+ borderRadius: 8,
208
+ },
209
+ modal: {
210
+ flex: 1,
211
+ backgroundColor,
212
+ justifyContent: 'center',
213
+ alignItems: 'center',
214
+ },
215
+ lightboxImage: {
216
+ height: '100%',
217
+ width: windowWidth,
218
+ backgroundColor,
219
+ },
220
+ lightboxImageLoading: {
221
+ backgroundColor,
222
+ },
223
+ actionToolbar: {
224
+ width: '100%',
225
+ position: 'absolute',
226
+ top: 0,
227
+ flexDirection: 'row',
228
+ alignItems: 'center',
229
+ gap: 20,
230
+ paddingHorizontal: 16,
231
+ paddingTop: 16,
232
+ paddingBottom: 8,
233
+ backgroundColor: transparentBackgroundColor,
234
+ },
235
+ actionToolbarTextMeta: {
236
+ flex: 1,
237
+ },
238
+ actionToolbarTitle: {
239
+ marginRight: 'auto',
240
+ flexShrink: 1,
241
+ color: tokens.colorNeutral88,
242
+ },
243
+ actionToolbarSubtitle: {
244
+ color: tokens.colorNeutral68,
245
+ },
246
+ actionButton: {
247
+ backgroundColor,
248
+ height: 40,
249
+ width: 40,
250
+ borderRadius: 50,
251
+ borderWidth: 1,
252
+ borderColor: tokens.colorNeutral24,
253
+ },
254
+ actionButtonIcon: {
255
+ color: tokens.colorNeutral88,
256
+ },
257
+ })
258
+ }
@@ -9,7 +9,11 @@ import { VideoAttachment } from './attachments/video_attachment'
9
9
  import { GiphyAttachment } from './attachments/giphy_attachment'
10
10
  import { GenericFileAttachment } from './attachments/generic_file_attachment'
11
11
  import { ExpandedLink } from './attachments/expanded_link'
12
- import { ImageAttachment, type MetaProps } from './attachments/image_attachment'
12
+ import { ImageAttachmentLegacy, type MetaProps } from './attachments/image_attachment_legacy'
13
+ import { ImageAttachment } from './attachments/image_attachment'
14
+
15
+ // Temporarily controls whether image attachments can be opened in a gallery lightbox. (Will remove after QA approves project.)
16
+ const ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY = false
13
17
 
14
18
  export function MessageAttachments(props: {
15
19
  attachments: DenormalizedAttachmentResource[]
@@ -20,14 +24,34 @@ export function MessageAttachments(props: {
20
24
  const styles = useStyles()
21
25
  const { attachments, metaProps, onMessageAttachmentLongPress, onMessageLongPress } = props
22
26
  if (!attachments || attachments.length === 0) return null
27
+
28
+ const imageAttachments = attachments.filter(
29
+ attachment =>
30
+ attachment.type === 'MessageAttachment' &&
31
+ attachment.attributes?.contentType?.startsWith('image/')
32
+ ) as DenormalizedMessageAttachmentResource[]
33
+
34
+ const showImageAttachmentGroup =
35
+ ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY && imageAttachments.length > 0
36
+
23
37
  return (
24
38
  <View style={styles.attachmentsContainer}>
25
- {attachments.map(attachment => {
39
+ {showImageAttachmentGroup &&
40
+ imageAttachments.map((image, index) => (
41
+ <ImageAttachment
42
+ key={`${image.id}-${index}`}
43
+ attachment={image}
44
+ metaProps={metaProps}
45
+ onMessageAttachmentLongPress={onMessageAttachmentLongPress}
46
+ />
47
+ ))}
48
+
49
+ {attachments.map((attachment, index) => {
26
50
  switch (attachment.type) {
27
51
  case 'MessageAttachment':
28
52
  return (
29
53
  <MessageAttachment
30
- key={attachment.id}
54
+ key={`${attachment.id}-${index}`}
31
55
  attachment={attachment}
32
56
  metaProps={metaProps}
33
57
  onMessageAttachmentLongPress={onMessageAttachmentLongPress}
@@ -36,7 +60,7 @@ export function MessageAttachments(props: {
36
60
  case 'giphy':
37
61
  return (
38
62
  <GiphyAttachment
39
- key={attachment.id || attachment.titleLink}
63
+ key={`${attachment.id || attachment.titleLink}-${index}`}
40
64
  attachment={attachment}
41
65
  onMessageLongPress={onMessageLongPress}
42
66
  />
@@ -44,7 +68,7 @@ export function MessageAttachments(props: {
44
68
  case 'ExpandedLink':
45
69
  return (
46
70
  <ExpandedLink
47
- key={attachment.id}
71
+ key={`${attachment.id}-${index}`}
48
72
  attachment={attachment}
49
73
  onMessageLongPress={onMessageLongPress}
50
74
  />
@@ -69,10 +93,15 @@ function MessageAttachment({
69
93
  const { attributes } = attachment
70
94
  const contentType = attributes?.contentType
71
95
  const basicType = contentType ? contentType.split('/')[0] : ''
96
+
97
+ if (basicType === 'image' && ENABLE_MESSAGE_ATTACHMENT_IMAGE_GALLERY) {
98
+ return null
99
+ }
100
+
72
101
  switch (basicType) {
73
102
  case 'image':
74
103
  return (
75
- <ImageAttachment
104
+ <ImageAttachmentLegacy
76
105
  attachment={attachment}
77
106
  metaProps={metaProps}
78
107
  onMessageAttachmentLongPress={onMessageAttachmentLongPress}