Skip to content

Commit e61c05c

Browse files
authored
feat: native image picker rework (#3398)
This pull request introduces extensive updates to the attachment picker functionality, focusing on adding a new `disableAttachmentPicker` mode. When enabled, this mode bypasses the custom attachment picker UI and instead uses native pickers or alternate flows for attachments and commands. The changes also include several refactorings to ensure that button behavior, UI rendering, and command handling adapt correctly to this new mode, as well as minor improvements to styles and performance. **Attachment Picker Disable Mode and UI Adaptation:** * Added `disableAttachmentPicker` support throughout the attachment picker and its context, defaulting to true if the image media library is unavailable. This flag is now used to conditionally render the custom picker UI and to switch to native pickers or alternate flows for attachments, commands, and polls. (`package/src/components/Channel/Channel.tsx` [[1]](diffhunk://#diff-f7139f4cdb523365cfc277d72b827a3432325b9c6460cf14628f9df67d0e4d85R544) [[2]](diffhunk://#diff-f7139f4cdb523365cfc277d72b827a3432325b9c6460cf14628f9df67d0e4d85L555-R556) [[3]](diffhunk://#diff-f7139f4cdb523365cfc277d72b827a3432325b9c6460cf14628f9df67d0e4d85L2047-R2047); `package/src/components/AttachmentPicker/AttachmentPicker.tsx` [[4]](diffhunk://#diff-509e397cee3ad5197cf9a2710f06f9cbd3d2e586f6c23ea908bf95f3ab576161R39) [[5]](diffhunk://#diff-509e397cee3ad5197cf9a2710f06f9cbd3d2e586f6c23ea908bf95f3ab576161R125-R129); `package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx` [[6]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1R40-R46) [[7]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1L54-R73) [[8]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1L79-R105) [[9]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1L112-R136) [[10]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1L133-R151) [[11]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1R161-R163) [[12]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1R178-R206) * Refactored all attachment picker buttons (`MediaPickerButton`, `CameraPickerButton`, `FilePickerButton`, `PollPickerButton`, `CommandsPickerButton`) to respect the `disableAttachmentPicker` flag, invoking native pickers or alternate flows as appropriate. (`package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx` [[1]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1L79-R105) [[2]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1L112-R136) [[3]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1L133-R151) [[4]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1R161-R163) [[5]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1R178-R206) **Command Picker and Sheet Handling:** * Updated the command picker logic to provide a bottom sheet modal for commands when the attachment picker is disabled, and adjusted command item press handling to close the sheet and focus the input. (`package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx` [[1]](diffhunk://#diff-c006c56f76295b093e39458e91083b0e0fc85cf255507c867cf2d1a6fac971c1R178-R206); `package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx` [[2]](diffhunk://#diff-dff257697e52bec2944a78a9987f720bd6c9950bed33d58dc6bbf46181ede85bL103-R164) * Refactored command picker item components to separate UI and logic, allowing for different behaviors based on the attachment picker mode. (`package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx` [package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsxL103-R164](diffhunk://#diff-dff257697e52bec2944a78a9987f720bd6c9950bed33d58dc6bbf46181ede85bL103-R164)) **Component and Style Improvements:** * Replaced usage of `BottomSheetFlatList` with `FlatList` in the command picker for improved compatibility and performance. (`package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx` [[1]](diffhunk://#diff-dff257697e52bec2944a78a9987f720bd6c9950bed33d58dc6bbf46181ede85bL18) [[2]](diffhunk://#diff-dff257697e52bec2944a78a9987f720bd6c9950bed33d58dc6bbf46181ede85bL172-R205) * Improved style definitions for better readability and maintainability, including minor fixes in selection bar and video attachment preview styles. (`examples/SampleApp/src/components/AttachmentPickerSelectionBar.tsx` [[1]](diffhunk://#diff-57b9f4c03eb9f2df19ce1eea85d2cf0b7faf0a582365a328b58972f726bd9d15L51-R56); `package/src/components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview.tsx` [[2]](diffhunk://#diff-330a667985a8bee41fcea36cb15f6ea4e35014752a052048c464a36983386feeL84-R84) [[3]](diffhunk://#diff-330a667985a8bee41fcea36cb15f6ea4e35014752a052048c464a36983386feeL104-R109) **Other Notable Changes:** * Increased the number of columns in the emoji reaction picker and disabled virtualization to improve animation performance. (`package/src/components/MessageMenu/MessageReactionPicker.tsx` [package/src/components/MessageMenu/MessageReactionPicker.tsxL164-R171](diffhunk://#diff-6300d8032dafbd2a4ee021a951f2566d63ba247cefa8b50b75fdf5a7bc87e1f2L164-R171)) * Cleaned up redundant code related to the attachment picker toggle and context usage. (`package/src/components/MessageInput/components/InputButtons/AttachButton.tsx` [[1]](diffhunk://#diff-decf71a1820966bd3433e3ba63c1a7a4c67f4dcecdd2bad18b6b5c39acd79cb8L25) [[2]](diffhunk://#diff-decf71a1820966bd3433e3ba63c1a7a4c67f4dcecdd2bad18b6b5c39acd79cb8L44-L46)
1 parent d3c5777 commit e61c05c

File tree

13 files changed

+377
-147
lines changed

13 files changed

+377
-147
lines changed

examples/SampleApp/src/components/AttachmentPickerSelectionBar.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,10 @@ export const CustomAttachmentPickerSelectionBar = () => {
4848
};
4949

5050
const styles = StyleSheet.create({
51-
selectionBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingBottom: 12, },
51+
selectionBar: {
52+
flexDirection: 'row',
53+
alignItems: 'center',
54+
paddingHorizontal: 16,
55+
paddingBottom: 12,
56+
},
5257
});

package/src/components/AttachmentPicker/AttachmentPicker.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const AttachmentPicker = () => {
3636
AttachmentPickerContent,
3737
attachmentPickerBottomSheetHeight,
3838
bottomSheetRef: ref,
39+
disableAttachmentPicker,
3940
} = useAttachmentPickerContext();
4041

4142
const [currentIndex, setCurrentIndexInternal] = useState(-1);
@@ -89,6 +90,8 @@ export const AttachmentPicker = () => {
8990
}
9091
}, [currentIndex, attachmentPickerStore]);
9192

93+
const selectionBarRef = useRef<number | null>(null);
94+
9295
const initialSnapPoint = attachmentPickerBottomSheetHeight;
9396

9497
/**
@@ -97,7 +100,6 @@ export const AttachmentPicker = () => {
97100
*/
98101
const snapPoints = useMemo(() => [initialSnapPoint], [initialSnapPoint]);
99102

100-
const selectionBarRef = useRef<number | null>(null);
101103
const onAttachmentPickerSelectionBarLayout = useStableCallback((e: LayoutChangeEvent) => {
102104
selectionBarRef.current = e.nativeEvent.layout.height;
103105
});
@@ -120,9 +122,11 @@ export const AttachmentPicker = () => {
120122
<View onLayout={onAttachmentPickerSelectionBarLayout}>
121123
<AttachmentPickerSelectionBar />
122124
</View>
123-
<AttachmentPickerContent
124-
height={attachmentPickerBottomSheetHeight - (selectionBarRef?.current ?? 0)}
125-
/>
125+
{!disableAttachmentPicker ? (
126+
<AttachmentPickerContent
127+
height={attachmentPickerBottomSheetHeight - (selectionBarRef?.current ?? 0)}
128+
/>
129+
) : null}
126130
</BottomSheet>
127131
);
128132
};

package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import { Linking, Platform, Pressable, StyleSheet, Text, View } from 'react-native';
33

4+
import { FlatList } from 'react-native-gesture-handler';
5+
46
import { CommandSearchSource, CommandSuggestion } from 'stream-chat';
57

68
import { AttachmentMediaPicker } from './AttachmentMediaPicker/AttachmentMediaPicker';
79

810
import {
11+
useAttachmentPickerContext,
12+
useBottomSheetContext,
913
useMessageComposer,
1014
useMessageInputContext,
1115
useTranslationContext,
@@ -15,7 +19,6 @@ import { useAttachmentPickerState, useStableCallback } from '../../../hooks';
1519
import { Camera, FilePickerIcon, IconProps, PollThumbnail, Recorder } from '../../../icons';
1620
import { primitives } from '../../../theme';
1721
import { CommandSuggestionItem } from '../../AutoCompleteInput/AutoCompleteSuggestionItem';
18-
import { BottomSheetFlatList } from '../../BottomSheetCompatibility/BottomSheetFlatList';
1922
import { Button } from '../../ui';
2023

2124
const useStyles = () => {
@@ -100,35 +103,65 @@ export const AttachmentPickerGenericContent = (props: AttachmentPickerGenericCon
100103

101104
const keyExtractor = (item: { id: string }) => item.id;
102105

103-
export const AttachmentCommantPickerItem = ({ item }: { item: CommandSuggestion }) => {
104-
const messageComposer = useMessageComposer();
105-
const { textComposer } = messageComposer;
106-
const { inputBoxRef } = useMessageInputContext();
107-
106+
const AttachmentCommandPickerItemUI = ({
107+
item,
108+
onPress,
109+
}: {
110+
item: CommandSuggestion;
111+
onPress: () => void;
112+
}) => {
108113
const {
109114
theme: { semantics },
110115
} = useTheme();
111116

112-
const handlePress = useCallback(() => {
113-
textComposer.setCommand(item);
114-
inputBoxRef.current?.focus();
115-
}, [item, textComposer, inputBoxRef]);
116-
117117
return (
118118
<Pressable
119119
style={({ pressed }) => ({
120120
backgroundColor: pressed ? semantics.backgroundCorePressed : undefined,
121121
borderRadius: primitives.radiusSm,
122122
})}
123-
onPress={handlePress}
123+
onPress={onPress}
124124
>
125125
<CommandSuggestionItem {...item} />
126126
</Pressable>
127127
);
128128
};
129129

130+
export const AttachmentCommandNativePickerItem = ({ item }: { item: CommandSuggestion }) => {
131+
const messageComposer = useMessageComposer();
132+
const { textComposer } = messageComposer;
133+
const { inputBoxRef } = useMessageInputContext();
134+
const { close } = useBottomSheetContext();
135+
136+
const handlePress = useCallback(() => {
137+
textComposer.setCommand(item);
138+
close(() => inputBoxRef.current?.focus());
139+
}, [textComposer, item, close, inputBoxRef]);
140+
141+
return <AttachmentCommandPickerItemUI item={item} onPress={handlePress} />;
142+
};
143+
144+
export const AttachmentCommandPickerItem = ({ item }: { item: CommandSuggestion }) => {
145+
const { disableAttachmentPicker } = useAttachmentPickerContext();
146+
147+
const messageComposer = useMessageComposer();
148+
const { textComposer } = messageComposer;
149+
const { inputBoxRef } = useMessageInputContext();
150+
151+
const handlePress = useCallback(() => {
152+
textComposer.setCommand(item);
153+
inputBoxRef.current?.focus();
154+
}, [textComposer, item, inputBoxRef]);
155+
156+
if (disableAttachmentPicker) {
157+
return <AttachmentCommandNativePickerItem item={item} />;
158+
}
159+
160+
return <AttachmentCommandPickerItemUI item={item} onPress={handlePress} />;
161+
};
162+
130163
const renderItem = ({ item }: { item: CommandSuggestion }) => {
131-
return <AttachmentCommantPickerItem item={item} />;
164+
return <AttachmentCommandPickerItem item={item} />;
132165
};
133166

134167
const useCommandPickerStyle = () => {
@@ -169,7 +202,7 @@ export const AttachmentCommandPicker = () => {
169202
return (
170203
<>
171204
<Text style={styles.title}>{t('Instant Commands')}</Text>
172-
<BottomSheetFlatList
205+
<FlatList
173206
contentContainerStyle={styles.contentContainer}
174207
renderItem={renderItem}
175208
data={commands}

package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useState } from 'react';
22

33
import { Platform, PressableProps, GestureResponderEvent } from 'react-native';
44

5+
import { AttachmentCommandPicker } from './AttachmentPickerContent';
6+
57
import {
68
useAttachmentPickerContext,
79
useChannelContext,
@@ -21,6 +23,7 @@ import {
2123
IconProps,
2224
} from '../../../icons';
2325
import { Button, ButtonProps } from '../../ui';
26+
import { BottomSheetModal } from '../../UIComponents';
2427

2528
export type AttachmentTypePickerButtonProps = Pick<ButtonProps, 'selected' | 'onPress'> & {
2629
Icon: ButtonProps['LeadingIcon'];
@@ -34,12 +37,13 @@ export const AttachmentTypePickerButton = ({
3437
onPress: onPressProp,
3538
Icon,
3639
}: AttachmentTypePickerButtonProps) => {
40+
const { disableAttachmentPicker } = useAttachmentPickerContext();
3741
const ButtonIcon = useCallback(
3842
(props: IconProps) => Icon && <Icon {...props} width={14} height={14} />,
3943
[Icon],
4044
);
4145
const onPress = useStableCallback((event: GestureResponderEvent) =>
42-
!selected && onPressProp ? onPressProp(event) : null,
46+
(!selected || disableAttachmentPicker) && onPressProp ? onPressProp(event) : null,
4347
);
4448
return (
4549
<Button
@@ -51,18 +55,22 @@ export const AttachmentTypePickerButton = ({
5155
size={'lg'}
5256
variant={'secondary'}
5357
iconOnly={true}
54-
selected={selected}
58+
selected={selected && !disableAttachmentPicker}
5559
/>
5660
);
5761
};
5862

5963
export const MediaPickerButton = () => {
60-
const { hasImagePicker } = useMessageInputContext();
61-
const { attachmentPickerStore } = useAttachmentPickerContext();
64+
const { hasImagePicker, pickAndUploadImageFromNativePicker } = useMessageInputContext();
65+
const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
6266
const { selectedPicker } = useAttachmentPickerState();
6367

6468
const setImagePicker = useStableCallback(() => {
65-
attachmentPickerStore.setSelectedPicker('images');
69+
if (disableAttachmentPicker) {
70+
pickAndUploadImageFromNativePicker();
71+
} else {
72+
attachmentPickerStore.setSelectedPicker('images');
73+
}
6674
});
6775

6876
return hasImagePicker ? (
@@ -76,17 +84,25 @@ export const MediaPickerButton = () => {
7684
};
7785

7886
export const CameraPickerButton = () => {
79-
const { attachmentPickerStore } = useAttachmentPickerContext();
87+
const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
8088
const { selectedPicker } = useAttachmentPickerState();
8189

82-
const { hasCameraPicker } = useMessageInputContext();
90+
const { hasCameraPicker, takeAndUploadImage } = useMessageInputContext();
8391

8492
const onCameraPickerPress = useStableCallback(() => {
85-
attachmentPickerStore.setSelectedPicker('camera-photo');
93+
if (disableAttachmentPicker) {
94+
takeAndUploadImage(Platform.OS === 'android' ? 'image' : 'mixed');
95+
} else {
96+
attachmentPickerStore.setSelectedPicker('camera-photo');
97+
}
8698
});
8799

88100
const onVideoRecorderPickerPress = useStableCallback(() => {
89-
attachmentPickerStore.setSelectedPicker('camera-video');
101+
if (disableAttachmentPicker) {
102+
takeAndUploadImage('video');
103+
} else {
104+
attachmentPickerStore.setSelectedPicker('camera-video');
105+
}
90106
});
91107

92108
return hasCameraPicker ? (
@@ -109,13 +125,15 @@ export const CameraPickerButton = () => {
109125
};
110126

111127
export const FilePickerButton = () => {
112-
const { attachmentPickerStore } = useAttachmentPickerContext();
128+
const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
113129
const { selectedPicker } = useAttachmentPickerState();
114130

115131
const { hasFilePicker, pickFile } = useMessageInputContext();
116132

117133
const openFilePicker = useStableCallback(() => {
118-
attachmentPickerStore.setSelectedPicker('files');
134+
if (!disableAttachmentPicker) {
135+
attachmentPickerStore.setSelectedPicker('files');
136+
}
119137
pickFile();
120138
});
121139

@@ -130,7 +148,7 @@ export const FilePickerButton = () => {
130148
};
131149

132150
export const PollPickerButton = () => {
133-
const { attachmentPickerStore } = useAttachmentPickerContext();
151+
const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
134152
const { selectedPicker } = useAttachmentPickerState();
135153

136154
const { threadList } = useChannelContext();
@@ -140,7 +158,9 @@ export const PollPickerButton = () => {
140158
const { openPollCreationDialog, sendMessage } = useMessageInputContext();
141159

142160
const openPollCreationModal = useStableCallback(() => {
143-
attachmentPickerStore.setSelectedPicker('polls');
161+
if (!disableAttachmentPicker) {
162+
attachmentPickerStore.setSelectedPicker('polls');
163+
}
144164
openPollCreationDialog?.({ sendMessage });
145165
});
146166

@@ -155,20 +175,34 @@ export const PollPickerButton = () => {
155175
};
156176

157177
export const CommandsPickerButton = () => {
178+
const [showCommandsSheet, setShowCommandsSheet] = useState(false);
158179
const { hasCommands } = useMessageInputContext();
159-
const { attachmentPickerStore } = useAttachmentPickerContext();
180+
const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
160181
const { selectedPicker } = useAttachmentPickerState();
161182

162183
const setCommandsPicker = useStableCallback(() => {
163-
attachmentPickerStore.setSelectedPicker('commands');
184+
if (disableAttachmentPicker) {
185+
setShowCommandsSheet(true);
186+
} else {
187+
attachmentPickerStore.setSelectedPicker('commands');
188+
}
164189
});
165190

191+
const onClose = useStableCallback(() => setShowCommandsSheet(false));
192+
166193
return hasCommands ? (
167-
<AttachmentTypePickerButton
168-
testID='commands-touchable'
169-
Icon={CommandsIcon}
170-
selected={selectedPicker === 'commands'}
171-
onPress={setCommandsPicker}
172-
/>
194+
<>
195+
<AttachmentTypePickerButton
196+
testID='commands-touchable'
197+
Icon={CommandsIcon}
198+
selected={selectedPicker === 'commands'}
199+
onPress={setCommandsPicker}
200+
/>
201+
{showCommandsSheet ? (
202+
<BottomSheetModal height={338} onClose={onClose} visible={showCommandsSheet} lazy={true}>
203+
<AttachmentCommandPicker />
204+
</BottomSheetModal>
205+
) : null}
206+
</>
173207
) : null;
174208
};

package/src/components/Channel/Channel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
541541

542542
const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) => {
543543
const {
544+
disableAttachmentPicker = !isImageMediaLibraryAvailable(),
544545
additionalKeyboardAvoidingViewProps,
545546
additionalPressableProps,
546547
additionalTextInputProps,
@@ -552,7 +553,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
552553
asyncMessagesSlideToCancelDistance = 75,
553554
AttachButton = AttachButtonDefault,
554555
Attachment = AttachmentDefault,
555-
attachmentPickerBottomSheetHeight = 333,
556+
attachmentPickerBottomSheetHeight = disableAttachmentPicker ? 72 : 333,
556557
AttachmentPickerSelectionBar = DefaultAttachmentPickerSelectionBar,
557558
attachmentSelectionBarHeight = 72,
558559
AudioAttachment = AudioAttachmentDefault,
@@ -583,7 +584,6 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
583584
customMessageSwipeAction,
584585
DateHeader = DateHeaderDefault,
585586
deletedMessagesVisibilityType = 'always',
586-
disableAttachmentPicker = !isImageMediaLibraryAvailable(),
587587
disableKeyboardCompatibleView = false,
588588
disableTypingIndicator,
589589
dismissKeyboardOnMessageTouch = true,
@@ -2044,7 +2044,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
20442044
<MessageInputProvider value={inputMessageInputContext}>
20452045
<AudioPlayerProvider value={audioPlayerContext}>
20462046
<View style={{ height: '100%' }}>{children}</View>
2047-
{!disableAttachmentPicker ? <AttachmentPicker /> : null}
2047+
<AttachmentPicker />
20482048
</AudioPlayerProvider>
20492049
</MessageInputProvider>
20502050
</MessageComposerProvider>

package/src/components/MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const useStyles = () => {
8181
},
8282
} = useTheme();
8383

84-
const { badgeBgInverse, badgeText } = semantics;
84+
const { badgeBgInverse, textInverse } = semantics;
8585

8686
return useMemo(
8787
() =>
@@ -101,11 +101,11 @@ const useStyles = () => {
101101
durationText: {
102102
fontSize: primitives.typographyFontSizeXxs,
103103
fontWeight: primitives.typographyFontWeightBold,
104-
color: badgeText,
104+
color: textInverse,
105105
marginLeft: primitives.spacingXxs,
106106
...durationText,
107107
},
108108
}),
109-
[badgeBgInverse, badgeText, durationContainer, durationText],
109+
[badgeBgInverse, textInverse, durationContainer, durationText],
110110
);
111111
};

0 commit comments

Comments
 (0)