diff --git a/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj b/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj index 51996b9be4..ab2d68ba57 100644 --- a/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj +++ b/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj @@ -284,10 +284,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-frameworks.sh\"\n"; @@ -302,6 +306,8 @@ "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", ); name = "[CP-User] [RNFB] Core Configuration"; + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; @@ -336,10 +342,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp/Pods-SampleApp-resources.sh\"\n"; @@ -353,10 +363,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-resources.sh\"\n"; @@ -370,10 +384,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SampleApp-SampleAppTests/Pods-SampleApp-SampleAppTests-frameworks.sh\"\n"; @@ -532,8 +550,8 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = SampleApp/SampleAppRelease.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 926; DEVELOPMENT_TEAM = EHV7XZLAHA; INFOPLIST_FILE = SampleApp/Info.plist; @@ -548,7 +566,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.getstream.reactnative.SampleApp; PRODUCT_NAME = SampleApp; - PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.reactnative.SampleApp"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -688,10 +706,7 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -823,10 +838,7 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/examples/SampleApp/src/components/AttachmentPickerContent.tsx b/examples/SampleApp/src/components/AttachmentPickerContent.tsx new file mode 100644 index 0000000000..a7a56e13ef --- /dev/null +++ b/examples/SampleApp/src/components/AttachmentPickerContent.tsx @@ -0,0 +1,53 @@ +import React, { useCallback, useState } from 'react'; +import { + useAttachmentPickerState, + AttachmentPickerContentProps, + AttachmentPickerContent, + AttachmentPickerGenericContent, + useStableCallback, + useTheme, + useTranslationContext, +} from 'stream-chat-react-native'; +import { ShareLocationIcon } from '../icons/ShareLocationIcon.tsx'; +import { LiveLocationCreateModal } from './LocationSharing/CreateLocationModal.tsx'; + +export const CustomAttachmentPickerContent = (props: AttachmentPickerContentProps) => { + const [modalVisible, setModalVisible] = useState(false); + const { selectedPicker } = useAttachmentPickerState(); + const { t } = useTranslationContext(); + const { + theme: { semantics }, + } = useTheme(); + + const Icon = useCallback( + () => , + [semantics.textTertiary], + ); + + const onRequestClose = () => { + setModalVisible(false); + }; + + const onOpenModal = useStableCallback(() => { + setModalVisible(true); + }); + + if (selectedPicker === 'location') { + return ( + <> + + {modalVisible ? ( + + ) : null} + + ); + } + + return ; +}; diff --git a/examples/SampleApp/src/components/AttachmentPickerSelectionBar.tsx b/examples/SampleApp/src/components/AttachmentPickerSelectionBar.tsx index 0b858bcb1c..9cc2b3f3d6 100644 --- a/examples/SampleApp/src/components/AttachmentPickerSelectionBar.tsx +++ b/examples/SampleApp/src/components/AttachmentPickerSelectionBar.tsx @@ -1,36 +1,52 @@ -import { useState } from 'react'; -import { Pressable, StyleSheet, View } from 'react-native'; -import { AttachmentPickerSelectionBar, useMessageInputContext } from 'stream-chat-react-native'; +import React, { useState } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { + AttachmentTypePickerButton, + useAttachmentPickerState, + CameraPickerButton, + CommandsPickerButton, + FilePickerButton, + MediaPickerButton, + PollPickerButton, + useAttachmentPickerContext, + useStableCallback, +} from 'stream-chat-react-native'; import { ShareLocationIcon } from '../icons/ShareLocationIcon'; import { LiveLocationCreateModal } from './LocationSharing/CreateLocationModal'; export const CustomAttachmentPickerSelectionBar = () => { const [modalVisible, setModalVisible] = useState(false); - const { closeAttachmentPicker } = useMessageInputContext(); + const { attachmentPickerStore } = useAttachmentPickerContext(); + const { selectedPicker } = useAttachmentPickerState(); const onRequestClose = () => { setModalVisible(false); - closeAttachmentPicker(); }; - const onOpenModal = () => { + const onOpenModal = useStableCallback(() => { + attachmentPickerStore.setSelectedPicker('location'); setModalVisible(true); - }; + }); return ( - - - - - + + + + + + + {modalVisible ? ( + + ) : null} ); }; const styles = StyleSheet.create({ - selectionBar: { flexDirection: 'row', alignItems: 'center' }, - liveLocationButton: { - paddingLeft: 4, - }, + selectionBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingBottom: 12, }, }); diff --git a/examples/SampleApp/src/icons/ShareLocationIcon.tsx b/examples/SampleApp/src/icons/ShareLocationIcon.tsx index 79c1e13584..2680f19261 100644 --- a/examples/SampleApp/src/icons/ShareLocationIcon.tsx +++ b/examples/SampleApp/src/icons/ShareLocationIcon.tsx @@ -1,24 +1,19 @@ import Svg, { Path } from 'react-native-svg'; -import { useTheme } from 'stream-chat-react-native'; +import { ColorValue } from 'react-native'; // Icon for "Share Location" button, next to input box. -export const ShareLocationIcon = () => { - const { - theme: { - colors: { grey }, - }, - } = useTheme(); +export const ShareLocationIcon = ({ stroke }: { stroke: ColorValue }) => { return ( - + ); diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx index 28b002c4f9..299d9abb6f 100644 --- a/examples/SampleApp/src/screens/ChannelScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelScreen.tsx @@ -33,6 +33,7 @@ import { MessageLocation } from '../components/LocationSharing/MessageLocation.t import { useStreamChatContext } from '../context/StreamChatContext.tsx'; import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx'; import { MessageInfoBottomSheet } from '../components/MessageInfoBottomSheet.tsx'; +import { CustomAttachmentPickerContent } from '../components/AttachmentPickerContent.tsx'; export type ChannelScreenNavigationProp = NativeStackNavigationProp< StackNavigatorParamList, @@ -223,6 +224,7 @@ export const ChannelScreen: React.FC = ({ { }); jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo); + +const BottomSheetMock = ({ handleComponent, children }) => ( + + {handleComponent()} + {children} + +); jest.mock('@gorhom/bottom-sheet', () => { const react = require('react-native'); return { @@ -48,8 +55,9 @@ jest.mock('@gorhom/bottom-sheet', () => { BottomSheetModal: react.View, BottomSheetModalProvider: react.View, BottomSheetScrollView: react.ScrollView, - default: react.View, + default: BottomSheetMock, TouchableOpacity: react.View, + useBottomSheetSpringConfigs: jest.fn(() => ({})), }; }); jest.mock('@op-engineering/op-sqlite', () => { diff --git a/package/src/components/AttachmentPicker/AttachmentPicker.tsx b/package/src/components/AttachmentPicker/AttachmentPicker.tsx index 59669fd70d..afdf0daac3 100644 --- a/package/src/components/AttachmentPicker/AttachmentPicker.tsx +++ b/package/src/components/AttachmentPicker/AttachmentPicker.tsx @@ -1,306 +1,132 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { BackHandler, EmitterSubscription, Keyboard, Platform, StyleSheet } from 'react-native'; - -import BottomSheetOriginal from '@gorhom/bottom-sheet'; -import type { BottomSheetHandleProps } from '@gorhom/bottom-sheet'; - +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { + BackHandler, + EmitterSubscription, + Keyboard, + Platform, + View, + LayoutChangeEvent, +} from 'react-native'; + +import { useBottomSheetSpringConfigs } from '@gorhom/bottom-sheet'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; -import type { AttachmentPickerErrorProps } from './components/AttachmentPickerError'; - -import { renderAttachmentPickerItem } from './components/AttachmentPickerItem'; - import { useAttachmentPickerContext } from '../../contexts/attachmentPickerContext/AttachmentPickerContext'; -import { MessageInputContextValue } from '../../contexts/messageInputContext/MessageInputContext'; -import { useTheme } from '../../contexts/themeContext/ThemeContext'; -import { useScreenDimensions } from '../../hooks/useScreenDimensions'; -import { NativeHandlers } from '../../native'; -import type { File } from '../../types/types'; +import { useStableCallback } from '../../hooks'; import { BottomSheet } from '../BottomSheetCompatibility/BottomSheet'; -import { BottomSheetFlatList } from '../BottomSheetCompatibility/BottomSheetFlatList'; import { KeyboardControllerPackage } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView'; dayjs.extend(duration); -const styles = StyleSheet.create({ - container: { - flexGrow: 1, - }, -}); - -export type AttachmentPickerProps = Pick< - MessageInputContextValue, - | 'AttachmentPickerBottomSheetHandle' - | 'attachmentPickerBottomSheetHandleHeight' - | 'attachmentSelectionBarHeight' - | 'attachmentPickerBottomSheetHeight' -> & { - /** - * Custom UI component to render error component while opening attachment picker. - * - * **Default** - * [AttachmentPickerError](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx) - */ - AttachmentPickerError: React.ComponentType; - /** - * Custom UI component to render error image for attachment picker - * - * **Default** - * [AttachmentPickerErrorImage](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/AttachmentPickerErrorImage.tsx) - */ - AttachmentPickerErrorImage: React.ComponentType; - /** - * Custom UI Component to render select more photos for selected gallery access in iOS. - */ - AttachmentPickerIOSSelectMorePhotos: React.ComponentType; - /** - * Custom UI component to render overlay component, that shows up on top of [selected - * image](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/1.png) (with tick mark) - * - * **Default** - * [ImageOverlaySelectedComponent](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx) - */ - ImageOverlaySelectedComponent: React.ComponentType; - attachmentPickerErrorButtonText?: string; - attachmentPickerErrorText?: string; - numberOfAttachmentImagesToLoadPerCall?: number; - numberOfAttachmentPickerImageColumns?: number; +const SPRING_CONFIG = { + damping: 80, + overshootClamping: true, + restDisplacementThreshold: 0.1, + restSpeedThreshold: 0.1, + stiffness: 500, + duration: 200, }; -export const AttachmentPicker = React.forwardRef( - (props: AttachmentPickerProps, ref: React.ForwardedRef) => { - const { - AttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight, - attachmentPickerBottomSheetHeight, - AttachmentPickerError, - attachmentPickerErrorButtonText, - AttachmentPickerErrorImage, - attachmentPickerErrorText, - AttachmentPickerIOSSelectMorePhotos, - ImageOverlaySelectedComponent, - numberOfAttachmentImagesToLoadPerCall, - numberOfAttachmentPickerImageColumns, - } = props; - - const { - theme: { - attachmentPicker: { bottomSheetContentContainer }, - colors: { white }, - }, - } = useTheme(); - const { closePicker, selectedPicker, setSelectedPicker, topInset } = - useAttachmentPickerContext(); - const { vh: screenVh } = useScreenDimensions(); - - const fullScreenHeight = screenVh(100); - - const [currentIndex, setCurrentIndex] = useState(-1); - const endCursorRef = useRef(undefined); - const [photoError, setPhotoError] = useState(false); - const [iOSLimited, setIosLimited] = useState(false); - const hasNextPageRef = useRef(true); - const [loadingPhotos, setLoadingPhotos] = useState(false); - const [photos, setPhotos] = useState([]); - const attemptedToLoadPhotosOnOpenRef = useRef(false); - - const getMorePhotos = useCallback(async () => { - if ( - hasNextPageRef.current && - !loadingPhotos && - currentIndex > -1 && - selectedPicker === 'images' - ) { - setPhotoError(false); - setLoadingPhotos(true); - const endCursor = endCursorRef.current; - try { - if (!NativeHandlers.getPhotos) { - setPhotos([]); - setIosLimited(false); - return; - } - const results = await NativeHandlers.getPhotos({ - after: endCursor, - first: numberOfAttachmentImagesToLoadPerCall ?? 60, - }); - endCursorRef.current = results.endCursor; - setPhotos((prevPhotos) => - endCursor ? [...prevPhotos, ...results.assets] : results.assets, - ); - setIosLimited(results.iOSLimited); - hasNextPageRef.current = !!results.hasNextPage; - } catch (error) { - setPhotoError(true); - } - setLoadingPhotos(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentIndex, selectedPicker, loadingPhotos]); - - // we need to use ref here to avoid running effect when getMorePhotos changes - const getMorePhotosRef = useRef(getMorePhotos); - getMorePhotosRef.current = getMorePhotos; - - useEffect(() => { - if (selectedPicker !== 'images') { - return; - } - - if (!NativeHandlers.oniOS14GalleryLibrarySelectionChange) { - return; - } - // ios 14 library selection change event is fired when user reselects the images that are permitted to be - // readable by the app - const { unsubscribe } = NativeHandlers.oniOS14GalleryLibrarySelectionChange(() => { - // we reset the cursor and has next page to true to facilitate fetching of the first page of photos again - hasNextPageRef.current = true; - endCursorRef.current = undefined; - // fetch the first page of photos again - getMorePhotosRef.current(); - }); - return unsubscribe; - }, [selectedPicker]); - - useEffect(() => { - const backAction = () => { - if (selectedPicker) { - setSelectedPicker(undefined); - closePicker(); - return true; - } - - return false; - }; - - const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction); - - return () => backHandler.remove(); - }, [selectedPicker, closePicker, setSelectedPicker]); - - useEffect(() => { - const onKeyboardOpenHandler = () => { - if (selectedPicker) { - setSelectedPicker(undefined); - } +export const AttachmentPicker = () => { + const { + closePicker, + attachmentPickerStore, + AttachmentPickerSelectionBar, + AttachmentPickerContent, + attachmentPickerBottomSheetHeight, + bottomSheetRef: ref, + } = useAttachmentPickerContext(); + + const [currentIndex, setCurrentIndexInternal] = useState(-1); + const currentIndexRef = useRef(currentIndex); + const setCurrentIndex = useStableCallback((_: number, toIndex: number) => { + setCurrentIndexInternal(toIndex); + currentIndexRef.current = toIndex; + }); + + useEffect(() => { + const backAction = () => { + if (attachmentPickerStore.state.getLatestValue().selectedPicker) { + attachmentPickerStore.setSelectedPicker(undefined); closePicker(); - }; - let keyboardSubscription: EmitterSubscription | null = null; - if (KeyboardControllerPackage?.KeyboardEvents) { - keyboardSubscription = KeyboardControllerPackage.KeyboardEvents.addListener( - 'keyboardWillShow', - onKeyboardOpenHandler, - ); - } else { - const keyboardShowEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; - keyboardSubscription = Keyboard.addListener(keyboardShowEvent, onKeyboardOpenHandler); + return true; } - return () => { - keyboardSubscription?.remove(); - }; - }, [closePicker, selectedPicker, setSelectedPicker]); - useEffect(() => { - if (currentIndex < 0) { - setSelectedPicker(undefined); - if (!loadingPhotos) { - endCursorRef.current = undefined; - hasNextPageRef.current = true; - attemptedToLoadPhotosOnOpenRef.current = false; - setPhotoError(false); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentIndex, loadingPhotos]); - - useEffect(() => { - if ( - !attemptedToLoadPhotosOnOpenRef.current && - selectedPicker === 'images' && - endCursorRef.current === undefined && - currentIndex > -1 && - !loadingPhotos - ) { - getMorePhotos(); - // we do this only once on open for avoiding to request permissions in rationale dialog again and again on - // Android - attemptedToLoadPhotosOnOpenRef.current = true; - } - }, [currentIndex, selectedPicker, getMorePhotos, loadingPhotos]); - - const selectedPhotos = useMemo( - () => - photos.map((asset) => ({ - asset, - ImageOverlaySelectedComponent, - numberOfAttachmentPickerImageColumns, - })), - [photos, ImageOverlaySelectedComponent, numberOfAttachmentPickerImageColumns], - ); - - const handleHeight = attachmentPickerBottomSheetHandleHeight; + return false; + }; - const initialSnapPoint = attachmentPickerBottomSheetHeight; + const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction); - const finalSnapPoint = fullScreenHeight - topInset; + return () => backHandler.remove(); + }, [attachmentPickerStore, closePicker]); - /** - * Snap points changing cause a rerender of the position, - * this is an issue if you are calling close on the bottom sheet. - */ - const snapPoints = [initialSnapPoint, finalSnapPoint]; + useEffect(() => { + const onKeyboardOpenHandler = () => { + if (attachmentPickerStore.state.getLatestValue().selectedPicker) { + attachmentPickerStore.setSelectedPicker(undefined); + } + closePicker(); + }; + let keyboardSubscription: EmitterSubscription | null = null; + if (KeyboardControllerPackage?.KeyboardEvents) { + keyboardSubscription = KeyboardControllerPackage.KeyboardEvents.addListener( + 'keyboardWillShow', + onKeyboardOpenHandler, + ); + } else { + const keyboardShowEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; + keyboardSubscription = Keyboard.addListener(keyboardShowEvent, onKeyboardOpenHandler); + } + return () => { + keyboardSubscription?.remove(); + }; + }, [attachmentPickerStore, closePicker]); + + useEffect(() => { + if (currentIndex < 0) { + attachmentPickerStore.setSelectedPicker(undefined); + } + }, [currentIndex, attachmentPickerStore]); + + const initialSnapPoint = attachmentPickerBottomSheetHeight; - const MemoizedAttachmentPickerBottomSheetHandle = useCallback( - (props: BottomSheetHandleProps) => - /** - * using `null` here instead of `style={{ opacity: photoError ? 0 : 1 }}` - * as opacity is not an allowed style - */ - !photoError && AttachmentPickerBottomSheetHandle ? ( - - ) : null, - [AttachmentPickerBottomSheetHandle, photoError], - ); + /** + * Snap points changing cause a rerender of the position, + * this is an issue if you are calling close on the bottom sheet. + */ + const snapPoints = useMemo(() => [initialSnapPoint], [initialSnapPoint]); + + const selectionBarRef = useRef(null); + const onAttachmentPickerSelectionBarLayout = useStableCallback((e: LayoutChangeEvent) => { + selectionBarRef.current = e.nativeEvent.layout.height; + }); + + const animationConfigs = useBottomSheetSpringConfigs(SPRING_CONFIG); + + return ( + + + + + + + ); +}; - return ( - <> - - {iOSLimited && } - item.asset.uri} - numColumns={numberOfAttachmentPickerImageColumns ?? 3} - onEndReached={photoError ? undefined : getMorePhotos} - renderItem={renderAttachmentPickerItem} - testID={'attachment-picker-list'} - /> - - {selectedPicker === 'images' && photoError && ( - - )} - - ); - }, -); +const RenderNull = () => null; AttachmentPicker.displayName = 'AttachmentPicker'; diff --git a/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.tsx b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.tsx new file mode 100644 index 0000000000..887a4d8080 --- /dev/null +++ b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentMediaPicker.tsx @@ -0,0 +1,184 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Linking, StyleSheet } from 'react-native'; + +import { renderAttachmentPickerItem } from './AttachmentPickerItem'; + +import { useAttachmentPickerContext, useTheme, useTranslationContext } from '../../../../contexts'; + +import { useStableCallback } from '../../../../hooks'; +import { Picture } from '../../../../icons'; + +import { NativeHandlers } from '../../../../native'; +import type { File } from '../../../../types/types'; +import { BottomSheetFlatList } from '../../../BottomSheetCompatibility/BottomSheetFlatList'; +import { + AttachmentPickerContentProps, + AttachmentPickerGenericContent, +} from '../AttachmentPickerContent'; + +export const IOS_LIMITED_DEEPLINK = '@getstream/ios-limited-button' as const; + +export type IosLimitedItemType = { uri: typeof IOS_LIMITED_DEEPLINK }; + +export type PhotoContentItemType = File | IosLimitedItemType; + +export const isIosLimited = (item: PhotoContentItemType): item is IosLimitedItemType => + 'uri' in item && item.uri === '@getstream/ios-limited-button'; + +const keyExtractor = (item: PhotoContentItemType) => item.uri; + +const useMediaPickerStyles = () => { + const { + theme: { semantics }, + } = useTheme(); + return StyleSheet.create({ + container: { + flexGrow: 1, + backgroundColor: semantics.composerBg, + }, + }); +}; + +export const AttachmentMediaPickerIcon = () => { + const { + theme: { semantics }, + } = useTheme(); + + return ; +}; + +export const AttachmentMediaPicker = (props: AttachmentPickerContentProps) => { + const { t } = useTranslationContext(); + const { numberOfAttachmentImagesToLoadPerCall, numberOfAttachmentPickerImageColumns } = + useAttachmentPickerContext(); + const { height } = props; + + const { + theme: { + attachmentPicker: { bottomSheetContentContainer }, + }, + } = useTheme(); + const styles = useMediaPickerStyles(); + + const numberOfColumns = numberOfAttachmentPickerImageColumns ?? 3; + + const endCursorRef = useRef(undefined); + const [photoError, setPhotoError] = useState(false); + const hasNextPageRef = useRef(true); + const loadingPhotosRef = useRef(false); + const [photos, setPhotos] = useState>([]); + const attemptedToLoadPhotosOnOpenRef = useRef(false); + + const getMorePhotos = useStableCallback(async () => { + if (hasNextPageRef.current && !loadingPhotosRef.current) { + setPhotoError(false); + loadingPhotosRef.current = true; + const endCursor = endCursorRef.current; + try { + if (!NativeHandlers.getPhotos) { + setPhotos([]); + return; + } + + const results = await NativeHandlers.getPhotos({ + after: endCursor, + first: numberOfAttachmentImagesToLoadPerCall ?? 25, + }); + + endCursorRef.current = results.endCursor; + // skip updating if the sheet closed in the meantime, to avoid + // confusing the bottom sheet internals + setPhotos((prevPhotos) => { + if (endCursor) { + return [...prevPhotos, ...results.assets]; + } + + let assets: PhotoContentItemType[] = results.assets; + + if (results.iOSLimited) { + assets = [{ uri: IOS_LIMITED_DEEPLINK }, ...assets]; + } + + for (let i = 0; i < results.assets.length; i++) { + if (assets[i].uri !== prevPhotos[i]?.uri) { + return assets; + } + } + + return prevPhotos.slice(0, assets.length); + }); + hasNextPageRef.current = !!results.hasNextPage; + } catch (error) { + setPhotoError(true); + } + loadingPhotosRef.current = false; + } + }); + + useEffect(() => { + if (!NativeHandlers.oniOS14GalleryLibrarySelectionChange) { + return; + } + // ios 14 library selection change event is fired when user reselects the images that are permitted to be + // readable by the app + const { unsubscribe } = NativeHandlers.oniOS14GalleryLibrarySelectionChange(() => { + // we reset the cursor and has next page to true to facilitate fetching of the first page of photos again + hasNextPageRef.current = true; + endCursorRef.current = undefined; + // fetch the first page of photos again + getMorePhotos(); + }); + return unsubscribe; + }, [getMorePhotos]); + + useEffect(() => { + if (!loadingPhotosRef.current) { + endCursorRef.current = undefined; + hasNextPageRef.current = true; + attemptedToLoadPhotosOnOpenRef.current = false; + setPhotoError(false); + } + }, []); + + useEffect(() => { + if ( + !attemptedToLoadPhotosOnOpenRef.current && + endCursorRef.current === undefined && + !loadingPhotosRef.current + ) { + getMorePhotos(); + // we do this only once on open for avoiding to request permissions in rationale dialog again and again on + // Android + attemptedToLoadPhotosOnOpenRef.current = true; + } + }, [getMorePhotos]); + + const openSettings = useStableCallback(async () => { + try { + await Linking.openSettings(); + } catch (error) { + console.log(error); + } + }); + + return photoError ? ( + + ) : ( + + ); +}; diff --git a/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx new file mode 100644 index 0000000000..6804f09564 --- /dev/null +++ b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx @@ -0,0 +1,228 @@ +import React from 'react'; + +import { Alert, ImageBackground, StyleSheet, Text, View } from 'react-native'; + +import { FileReference, isLocalImageAttachment, isLocalVideoAttachment } from 'stream-chat'; + +import { isIosLimited, PhotoContentItemType } from './AttachmentMediaPicker'; + +import { useAttachmentPickerContext } from '../../../../contexts'; +import { useAttachmentManagerState } from '../../../../contexts/messageInputContext/hooks/useAttachmentManagerState'; +import { useMessageComposer } from '../../../../contexts/messageInputContext/hooks/useMessageComposer'; +import { useMessageInputContext } from '../../../../contexts/messageInputContext/MessageInputContext'; +import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; +import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext'; +import { useViewport } from '../../../../hooks/useViewport'; +import { NewPlus } from '../../../../icons/NewPlus'; +import { NativeHandlers } from '../../../../native'; +import { primitives } from '../../../../theme'; +import type { File } from '../../../../types/types'; +import { BottomSheetTouchableOpacity } from '../../../BottomSheetCompatibility/BottomSheetTouchableOpacity'; +import { VideoAttachmentMetadataPill } from '../../../MessageInput/components/AttachmentPreview/VideoAttachmentUploadPreview'; + +type AttachmentPickerItemType = { + asset: File; +}; + +const AttachmentVideo = (props: AttachmentPickerItemType) => { + const { asset } = props; + const { numberOfAttachmentPickerImageColumns, ImageOverlaySelectedComponent } = + useAttachmentPickerContext(); + const { vw } = useViewport(); + const { t } = useTranslationContext(); + const messageComposer = useMessageComposer(); + const { uploadNewFile } = useMessageInputContext(); + const { attachmentManager } = messageComposer; + const { attachments, availableUploadSlots } = useAttachmentManagerState(); + + const selectedIndex = attachments.findIndex((attachment) => + isLocalVideoAttachment(attachment) + ? (attachment.localMetadata.file as FileReference).uri === asset.uri + : false, + ); + + const { + theme: { + attachmentPicker: { image, imageOverlay }, + }, + } = useTheme(); + const styles = useStyles(); + + const { duration: videoDuration, thumb_url } = asset; + + const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2; + + const onPressVideo = async () => { + if (selectedIndex !== -1) { + const attachment = attachments[selectedIndex]; + if (attachment) { + attachmentManager.removeAttachments([attachment.localMetadata.id]); + } + } else { + if (!availableUploadSlots) { + Alert.alert(t('Maximum number of files reached')); + return; + } + await uploadNewFile(asset); + } + }; + + return ( + + + + + + + + + ); +}; + +const AttachmentImage = (props: AttachmentPickerItemType) => { + const { asset } = props; + const { numberOfAttachmentPickerImageColumns, ImageOverlaySelectedComponent } = + useAttachmentPickerContext(); + const { + theme: { + attachmentPicker: { image, imageOverlay }, + }, + } = useTheme(); + const styles = useStyles(); + const { vw } = useViewport(); + const { uploadNewFile } = useMessageInputContext(); + const messageComposer = useMessageComposer(); + const { attachmentManager } = messageComposer; + const { attachments, availableUploadSlots } = useAttachmentManagerState(); + const selectedIndex = attachments.findIndex((attachment) => + isLocalImageAttachment(attachment) ? attachment.localMetadata.previewUri === asset.uri : false, + ); + + const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2; + + const { uri } = asset; + + const onPressImage = async () => { + if (selectedIndex !== -1) { + const attachment = attachments[selectedIndex]; + if (attachment) { + await attachmentManager.removeAttachments([attachment.localMetadata.id]); + } + } else { + if (!availableUploadSlots) { + Alert.alert('Maximum number of files reached'); + return; + } + await uploadNewFile(asset); + } + }; + + return ( + + + + + + + + ); +}; + +const AttachmentIosLimited = () => { + const { numberOfAttachmentPickerImageColumns } = useAttachmentPickerContext(); + const { vw } = useViewport(); + const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2; + const styles = useStyles(); + return ( + + + Add more + + ); +}; + +export const renderAttachmentPickerItem = ({ item }: { item: PhotoContentItemType }) => { + if (isIosLimited(item)) { + return ; + } + /** + * Expo Media Library - Result of asset type + * Native Android - Gives mime type(Eg: image/jpeg, video/mp4, etc.) + * Native iOS - Gives `image` or `video` + * Expo Android/iOS - Gives `photo` or `video` + **/ + const isVideoType = item.type.includes('video'); + + if (isVideoType) { + return ; + } + + return ; +}; + +const useStyles = () => { + const { + theme: { semantics }, + } = useTheme(); + return StyleSheet.create({ + durationText: { + fontWeight: 'bold', + }, + overlay: { + alignItems: 'flex-end', + flex: 1, + }, + videoView: { + bottom: 5, + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + paddingHorizontal: 5, + position: 'absolute', + width: '100%', + }, + iosLimitedContainer: { + margin: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: semantics.backgroundCoreSurfaceSubtle, + }, + iosLimitedIcon: { + color: semantics.textTertiary, + }, + iosLimitedText: { + fontWeight: primitives.typographyFontWeightSemiBold, + fontSize: primitives.typographyFontSizeSm, + color: semantics.textTertiary, + }, + }); +}; diff --git a/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/index.ts b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/index.ts new file mode 100644 index 0000000000..a803c5a324 --- /dev/null +++ b/package/src/components/AttachmentPicker/components/AttachmentMediaPicker/index.ts @@ -0,0 +1,2 @@ +export * from './AttachmentMediaPicker'; +export * from './AttachmentPickerItem'; diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerBottomSheetHandle.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerBottomSheetHandle.tsx deleted file mode 100644 index c48f63b785..0000000000 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerBottomSheetHandle.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { StyleSheet, View, ViewStyle } from 'react-native'; -import Animated, { SharedValue, useAnimatedStyle } from 'react-native-reanimated'; - -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; - -const styles = StyleSheet.create({ - container: { - alignItems: 'center', - height: 20, - justifyContent: 'center', - }, - handle: { - borderRadius: 2, - height: 4, - width: 40, - }, -}); - -type Props = { - animatedIndex: SharedValue; -}; - -export const AttachmentPickerBottomSheetHandle = ({ animatedIndex }: Props) => { - const { - theme: { - attachmentPicker: { - handle: { container, indicator }, - }, - colors: { black, white }, - }, - } = useTheme(); - - const style = useAnimatedStyle(() => ({ - borderTopLeftRadius: animatedIndex.value > 0 ? 16 - animatedIndex.value * 16 : 16, - borderTopRightRadius: animatedIndex.value > 0 ? 16 - animatedIndex.value * 16 : 16, - })); - - return ( - - - {/* ^ 1A = 10% opacity */} - - ); -}; diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx new file mode 100644 index 0000000000..5802deeb45 --- /dev/null +++ b/package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx @@ -0,0 +1,303 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Linking, Platform, Pressable, StyleSheet, Text, View } from 'react-native'; + +import { CommandSearchSource, CommandSuggestion } from 'stream-chat'; + +import { AttachmentMediaPicker } from './AttachmentMediaPicker/AttachmentMediaPicker'; + +import { + useMessageComposer, + useMessageInputContext, + useTranslationContext, +} from '../../../contexts'; +import { useTheme } from '../../../contexts/themeContext/ThemeContext'; +import { useAttachmentPickerState, useStableCallback } from '../../../hooks'; +import { Camera, FilePickerIcon, IconProps, PollThumbnail, Recorder } from '../../../icons'; +import { primitives } from '../../../theme'; +import { CommandSuggestionItem } from '../../AutoCompleteInput/AutoCompleteSuggestionItem'; +import { BottomSheetFlatList } from '../../BottomSheetCompatibility/BottomSheetFlatList'; +import { Button } from '../../ui'; + +const useStyles = () => { + const { + theme: { semantics }, + } = useTheme(); + + return useMemo( + () => + StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + backgroundColor: semantics.backgroundElevationElevation1, + paddingHorizontal: primitives.spacing2xl, + paddingBottom: primitives.spacing3xl, + }, + infoContainer: { flex: 1, alignItems: 'center', justifyContent: 'center' }, + text: { + fontSize: primitives.typographyFontSizeMd, + color: semantics.textSecondary, + marginTop: 8, + marginHorizontal: 24, + textAlign: 'center', + maxWidth: 200, + }, + }), + [semantics.backgroundElevationElevation1, semantics.textSecondary], + ); +}; + +export type AttachmentPickerGenericContentProps = { + Icon: React.ComponentType; + onPress: () => void; + height?: number; + buttonText?: string; + description?: string; +}; + +export const AttachmentPickerGenericContent = (props: AttachmentPickerGenericContentProps) => { + const { height, buttonText, Icon, description, onPress } = props; + const styles = useStyles(); + + const { + theme: { + semantics, + attachmentPicker: { + content: { container, text, infoContainer }, + }, + }, + } = useTheme(); + + const ThemedIcon = useCallback( + () => , + [Icon, semantics.textTertiary], + ); + + return ( + + + + {description} + + + + ); +}; + +const keyExtractor = (item: { id: string }) => item.id; + +export const AttachmentCommantPickerItem = ({ item }: { item: CommandSuggestion }) => { + const messageComposer = useMessageComposer(); + const { textComposer } = messageComposer; + const { inputBoxRef } = useMessageInputContext(); + + const { + theme: { semantics }, + } = useTheme(); + + const handlePress = useCallback(() => { + textComposer.setCommand(item); + inputBoxRef.current?.focus(); + }, [item, textComposer, inputBoxRef]); + + return ( + ({ + backgroundColor: pressed ? semantics.backgroundCorePressed : undefined, + borderRadius: primitives.radiusSm, + })} + onPress={handlePress} + > + + + ); +}; + +const renderItem = ({ item }: { item: CommandSuggestion }) => { + return ; +}; + +const useCommandPickerStyle = () => { + const { + theme: { semantics }, + } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + contentContainer: { + flexGrow: 1, + paddingHorizontal: primitives.spacingXxs, + paddingBottom: primitives.spacing2xl, + }, + title: { + fontWeight: primitives.typographyFontWeightSemiBold, + fontSize: primitives.typographyFontSizeMd, + color: semantics.textPrimary, + paddingHorizontal: primitives.spacingMd, + paddingBottom: primitives.spacingMd, + }, + }), + [semantics.textPrimary], + ); +}; + +export const AttachmentCommandPicker = () => { + const { t } = useTranslationContext(); + const messageComposer = useMessageComposer(); + const [commands] = useState(() => { + const commandsSearchSource = new CommandSearchSource(messageComposer.channel); + const result = commandsSearchSource.query(''); + + return result.items; + }); + const styles = useCommandPickerStyle(); + + return ( + <> + {t('Instant Commands')} + + + ); +}; + +export const AttachmentPollPicker = (props: AttachmentPickerContentProps) => { + const { t } = useTranslationContext(); + const { height } = props; + const { openPollCreationDialog, sendMessage } = useMessageInputContext(); + + const openPollCreationModal = useStableCallback(() => { + openPollCreationDialog?.({ sendMessage }); + }); + + return ( + + ); +}; + +export const AttachmentCameraPicker = ( + props: AttachmentPickerContentProps & { videoOnly?: boolean }, +) => { + const [permissionDenied, setPermissionDenied] = useState(false); + const { t } = useTranslationContext(); + const { height, videoOnly } = props; + const { takeAndUploadImage } = useMessageInputContext(); + + const openCameraPicker = useStableCallback(async () => { + const result = await takeAndUploadImage( + videoOnly ? 'video' : Platform.OS === 'android' ? 'image' : 'mixed', + ); + if (result?.askToOpenSettings) { + setPermissionDenied(true); + } + }); + + const openSettings = useStableCallback(async () => { + try { + await Linking.openSettings(); + } catch (error) { + console.log(error); + } + }); + + useEffect(() => { + openCameraPicker(); + }, [openCameraPicker]); + + return permissionDenied ? ( + + ) : ( + + ); +}; + +export const AttachmentFilePicker = (props: AttachmentPickerContentProps) => { + const { t } = useTranslationContext(); + const { height } = props; + const { pickFile } = useMessageInputContext(); + + return ( + + ); +}; + +export type AttachmentPickerContentProps = Pick; + +export const AttachmentPickerContent = (props: AttachmentPickerContentProps) => { + const { height } = props; + const { selectedPicker } = useAttachmentPickerState(); + + // TODO V9: Think of a better way to do this. This is just a temporary fix. + const lastSelectedPickerRef = useRef(selectedPicker); + if (selectedPicker) { + lastSelectedPickerRef.current = selectedPicker; + } + + if (lastSelectedPickerRef.current === 'images') { + return ; + } + + if (lastSelectedPickerRef.current === 'files') { + return ; + } + + if (lastSelectedPickerRef.current === 'camera-photo') { + return ; + } + + if (lastSelectedPickerRef.current === 'camera-video') { + return ; + } + + if (lastSelectedPickerRef.current === 'polls') { + return ; + } + + if (lastSelectedPickerRef.current === 'commands') { + return ; + } + + return null; +}; diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx deleted file mode 100644 index 008da8c640..0000000000 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import { Linking, StyleSheet, Text, View } from 'react-native'; - -import { useAttachmentPickerContext } from '../../../contexts/attachmentPickerContext/AttachmentPickerContext'; -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; - -const styles = StyleSheet.create({ - errorButtonText: { - fontSize: 14, - fontWeight: '600', - marginHorizontal: 24, - marginTop: 16, - textAlign: 'center', - }, - errorContainer: { - alignItems: 'center', - bottom: 0, - justifyContent: 'center', - left: 0, - position: 'absolute', - right: 0, - }, - errorText: { - fontSize: 14, - marginHorizontal: 24, - marginTop: 16, - textAlign: 'center', - }, -}); - -export type AttachmentPickerErrorProps = { - AttachmentPickerErrorImage: React.ComponentType; - attachmentPickerBottomSheetHeight?: number; - attachmentPickerErrorButtonText?: string; - attachmentPickerErrorText?: string; -}; - -export const AttachmentPickerError = (props: AttachmentPickerErrorProps) => { - const { - attachmentPickerBottomSheetHeight, - attachmentPickerErrorButtonText, - AttachmentPickerErrorImage, - attachmentPickerErrorText, - } = props; - - const { - theme: { - attachmentPicker: { errorButtonText, errorContainer, errorText }, - colors: { accent_blue, grey, white_smoke }, - }, - } = useTheme(); - const { t } = useTranslationContext(); - - const { closePicker, setSelectedPicker } = useAttachmentPickerContext(); - - const openSettings = async () => { - try { - setSelectedPicker(undefined); - closePicker(); - await Linking.openSettings(); - } catch (error) { - console.log(error); - } - }; - - return ( - - - - {attachmentPickerErrorText || - t('Please enable access to your photos and videos so you can share them.')} - - - {attachmentPickerErrorButtonText || t('Allow access to your Gallery')} - - - ); -}; diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerErrorImage.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerErrorImage.tsx deleted file mode 100644 index bad5c674ad..0000000000 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerErrorImage.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { Picture } from '../../../icons'; - -export const AttachmentPickerErrorImage = () => { - const { - theme: { - colors: { grey_gainsboro }, - }, - } = useTheme(); - - return ; -}; diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos.tsx deleted file mode 100644 index 1654a8b52f..0000000000 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Pressable, StyleSheet, Text } from 'react-native'; - -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; -import { NativeHandlers } from '../../../native'; - -export const AttachmentPickerIOSSelectMorePhotos = () => { - const { t } = useTranslationContext(); - const { - theme: { - colors: { accent_blue, white }, - }, - } = useTheme(); - - if (!NativeHandlers.iOS14RefreshGallerySelection) { - return null; - } - - return ( - - {t('Select More Photos')} - - ); -}; - -const styles = StyleSheet.create({ - container: {}, - text: { - fontSize: 16, - paddingVertical: 10, - textAlign: 'center', - }, -}); diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx deleted file mode 100644 index fb3bc84acb..0000000000 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerItem.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import React from 'react'; - -import { Alert, ImageBackground, StyleSheet, Text, View } from 'react-native'; - -import { FileReference, isLocalImageAttachment, isLocalVideoAttachment } from 'stream-chat'; - -import { useAttachmentManagerState } from '../../../contexts/messageInputContext/hooks/useAttachmentManagerState'; -import { useMessageComposer } from '../../../contexts/messageInputContext/hooks/useMessageComposer'; -import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext'; -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; -import { useViewport } from '../../../hooks/useViewport'; -import { Recorder } from '../../../icons'; -import type { File } from '../../../types/types'; -import { getDurationLabelFromDuration } from '../../../utils/utils'; -import { BottomSheetTouchableOpacity } from '../../BottomSheetCompatibility/BottomSheetTouchableOpacity'; - -type AttachmentPickerItemType = { - asset: File; - ImageOverlaySelectedComponent: React.ComponentType; - numberOfAttachmentPickerImageColumns?: number; -}; - -const AttachmentVideo = (props: AttachmentPickerItemType) => { - const { asset, ImageOverlaySelectedComponent, numberOfAttachmentPickerImageColumns } = props; - const { vw } = useViewport(); - const { t } = useTranslationContext(); - const messageComposer = useMessageComposer(); - const { uploadNewFile } = useMessageInputContext(); - const { attachmentManager } = messageComposer; - const { attachments, availableUploadSlots } = useAttachmentManagerState(); - const videoUploads = attachments.filter((attachment) => isLocalVideoAttachment(attachment)); - - const selected = videoUploads.some( - (attachment) => (attachment.localMetadata.file as FileReference).uri === asset.uri, - ); - - const { - theme: { - attachmentPicker: { durationText, image, imageOverlay }, - colors: { overlay, white }, - }, - } = useTheme(); - - const { duration: videoDuration, thumb_url, uri } = asset; - - const durationLabel = videoDuration ? getDurationLabelFromDuration(videoDuration) : '00:00'; - - const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2; - - const onPressVideo = async () => { - if (selected) { - const attachment = videoUploads.find( - (attachment) => (attachment.localMetadata.file as FileReference).uri === uri, - ); - if (attachment) { - attachmentManager.removeAttachments([attachment.localMetadata.id]); - } - } else { - if (!availableUploadSlots) { - Alert.alert(t('Maximum number of files reached')); - return; - } - await uploadNewFile(asset); - } - }; - - return ( - - - {selected && ( - - - - )} - - - {videoDuration ? ( - - {durationLabel} - - ) : null} - - - - ); -}; - -const AttachmentImage = (props: AttachmentPickerItemType) => { - const { asset, ImageOverlaySelectedComponent, numberOfAttachmentPickerImageColumns } = props; - const { - theme: { - attachmentPicker: { image, imageOverlay }, - colors: { overlay }, - }, - } = useTheme(); - const { vw } = useViewport(); - const { uploadNewFile } = useMessageInputContext(); - const messageComposer = useMessageComposer(); - const { attachmentManager } = messageComposer; - const { attachments, availableUploadSlots } = useAttachmentManagerState(); - const imageUploads = attachments.filter((attachment) => isLocalImageAttachment(attachment)); - - const selected = imageUploads.some( - (attachment) => attachment.localMetadata.previewUri === asset.uri, - ); - - const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2; - - const { uri } = asset; - - const onPressImage = async () => { - if (selected) { - const attachment = imageUploads.find( - (attachment) => attachment.localMetadata.previewUri === uri, - ); - if (attachment) { - await attachmentManager.removeAttachments([attachment.localMetadata.id]); - } - } else { - if (!availableUploadSlots) { - Alert.alert('Maximum number of files reached'); - return; - } - await uploadNewFile(asset); - } - }; - - return ( - - - {selected && ( - - - - )} - - - ); -}; - -export const renderAttachmentPickerItem = ({ item }: { item: AttachmentPickerItemType }) => { - const { asset, ImageOverlaySelectedComponent, numberOfAttachmentPickerImageColumns } = item; - - /** - * Expo Media Library - Result of asset type - * Native Android - Gives mime type(Eg: image/jpeg, video/mp4, etc.) - * Native iOS - Gives `image` or `video` - * Expo Android/iOS - Gives `photo` or `video` - **/ - const isVideoType = asset.type.includes('video'); - - if (isVideoType) { - return ( - - ); - } - - return ( - - ); -}; - -const styles = StyleSheet.create({ - durationText: { - fontWeight: 'bold', - }, - overlay: { - alignItems: 'flex-end', - flex: 1, - }, - videoView: { - bottom: 5, - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - paddingHorizontal: 5, - position: 'absolute', - width: '100%', - }, -}); diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx index f2bceb7e61..c43274a096 100644 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx +++ b/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx @@ -1,142 +1,51 @@ -import React from 'react'; -import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native'; +import React, { useMemo } from 'react'; +import { StyleSheet, View } from 'react-native'; -import { useAttachmentPickerContext } from '../../../contexts/attachmentPickerContext/AttachmentPickerContext'; -import { useChannelContext } from '../../../contexts/channelContext/ChannelContext'; -import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext'; -import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext'; -import { useOwnCapabilitiesContext } from '../../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext'; -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; - -const styles = StyleSheet.create({ - container: { - alignItems: 'center', - flexDirection: 'row', - paddingHorizontal: 6, - }, - icon: { - marginHorizontal: 12, - }, -}); +import { + CameraPickerButton, + CommandsPickerButton, + FilePickerButton, + MediaPickerButton, + PollPickerButton, +} from './AttachmentTypePickerButton'; -export const AttachmentPickerSelectionBar = () => { - const { closePicker, selectedPicker, setSelectedPicker } = useAttachmentPickerContext(); +import { useTheme } from '../../../contexts/themeContext/ThemeContext'; +import { primitives } from '../../../theme'; +const useStyles = () => { const { - attachmentSelectionBarHeight, - CameraSelectorIcon, - CreatePollIcon, - FileSelectorIcon, - hasCameraPicker, - hasFilePicker, - hasImagePicker, - ImageSelectorIcon, - openPollCreationDialog, - pickFile, - sendMessage, - takeAndUploadImage, - VideoRecorderSelectorIcon, - } = useMessageInputContext(); - const { threadList } = useChannelContext(); - const { hasCreatePoll } = useMessagesContext(); - const ownCapabilities = useOwnCapabilitiesContext(); + theme: { semantics }, + } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + backgroundColor: semantics.composerBg, + paddingBottom: primitives.spacingSm, + paddingHorizontal: primitives.spacingMd, + alignItems: 'center', + flexDirection: 'row', + }, + }), + [semantics.composerBg], + ); +}; +export const AttachmentPickerSelectionBar = () => { const { theme: { - attachmentSelectionBar: { container, icon }, + attachmentSelectionBar: { container }, }, } = useTheme(); - - const setImagePicker = () => { - if (selectedPicker === 'images') { - setSelectedPicker(undefined); - closePicker(); - } else { - setSelectedPicker('images'); - } - }; - - const openFilePicker = () => { - setSelectedPicker(undefined); - closePicker(); - pickFile(); - }; - - const openPollCreationModal = () => { - setSelectedPicker(undefined); - closePicker(); - openPollCreationDialog?.({ sendMessage }); - }; - - const onCameraPickerPress = () => { - setSelectedPicker(undefined); - closePicker(); - takeAndUploadImage(Platform.OS === 'android' ? 'image' : 'mixed'); - }; - - const onVideoRecorderPickerPress = () => { - setSelectedPicker(undefined); - closePicker(); - takeAndUploadImage('video'); - }; + const styles = useStyles(); return ( - - {hasImagePicker ? ( - - - - - - ) : null} - {hasFilePicker ? ( - - - - - - ) : null} - {hasCameraPicker ? ( - - - - - - ) : null} - {hasCameraPicker && Platform.OS === 'android' ? ( - - - - - - ) : null} - {!threadList && hasCreatePoll && ownCapabilities.sendPoll ? ( // do not allow poll creation in threads - - - - - - ) : null} + + + + + + ); }; diff --git a/package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx b/package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx new file mode 100644 index 0000000000..7441caf0a3 --- /dev/null +++ b/package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx @@ -0,0 +1,174 @@ +import React, { useCallback } from 'react'; + +import { Platform, PressableProps, GestureResponderEvent } from 'react-native'; + +import { + useAttachmentPickerContext, + useChannelContext, + useMessageInputContext, + useMessagesContext, + useOwnCapabilitiesContext, +} from '../../../contexts'; +import { useStableCallback } from '../../../hooks'; +import { useAttachmentPickerState } from '../../../hooks/useAttachmentPickerState'; +import { + Camera, + Picture, + Recorder, + FilePickerIcon, + PollThumbnail, + CommandsIcon, + IconProps, +} from '../../../icons'; +import { Button, ButtonProps } from '../../ui'; + +export type AttachmentTypePickerButtonProps = Pick & { + Icon: ButtonProps['LeadingIcon']; +} & Pick; + +const hitSlop = { bottom: 15, top: 15 }; + +export const AttachmentTypePickerButton = ({ + testID, + selected, + onPress: onPressProp, + Icon, +}: AttachmentTypePickerButtonProps) => { + const ButtonIcon = useCallback( + (props: IconProps) => Icon && , + [Icon], + ); + const onPress = useStableCallback((event: GestureResponderEvent) => + !selected && onPressProp ? onPressProp(event) : null, + ); + return ( + + ); +}; + +export const MediaPickerButton = () => { + const { hasImagePicker } = useMessageInputContext(); + const { attachmentPickerStore } = useAttachmentPickerContext(); + const { selectedPicker } = useAttachmentPickerState(); + + const setImagePicker = useStableCallback(() => { + attachmentPickerStore.setSelectedPicker('images'); + }); + + return hasImagePicker ? ( + + ) : null; +}; + +export const CameraPickerButton = () => { + const { attachmentPickerStore } = useAttachmentPickerContext(); + const { selectedPicker } = useAttachmentPickerState(); + + const { hasCameraPicker } = useMessageInputContext(); + + const onCameraPickerPress = useStableCallback(() => { + attachmentPickerStore.setSelectedPicker('camera-photo'); + }); + + const onVideoRecorderPickerPress = useStableCallback(() => { + attachmentPickerStore.setSelectedPicker('camera-video'); + }); + + return hasCameraPicker ? ( + <> + + {Platform.OS === 'android' ? ( + + ) : null} + + ) : null; +}; + +export const FilePickerButton = () => { + const { attachmentPickerStore } = useAttachmentPickerContext(); + const { selectedPicker } = useAttachmentPickerState(); + + const { hasFilePicker, pickFile } = useMessageInputContext(); + + const openFilePicker = useStableCallback(() => { + attachmentPickerStore.setSelectedPicker('files'); + pickFile(); + }); + + return hasFilePicker ? ( + + ) : null; +}; + +export const PollPickerButton = () => { + const { attachmentPickerStore } = useAttachmentPickerContext(); + const { selectedPicker } = useAttachmentPickerState(); + + const { threadList } = useChannelContext(); + const { hasCreatePoll } = useMessagesContext(); + const ownCapabilities = useOwnCapabilitiesContext(); + + const { openPollCreationDialog, sendMessage } = useMessageInputContext(); + + const openPollCreationModal = useStableCallback(() => { + attachmentPickerStore.setSelectedPicker('polls'); + openPollCreationDialog?.({ sendMessage }); + }); + + return !threadList && hasCreatePoll && ownCapabilities.sendPoll ? ( // do not allow poll creation in threads + + ) : null; +}; + +export const CommandsPickerButton = () => { + const { hasCommands } = useMessageInputContext(); + const { attachmentPickerStore } = useAttachmentPickerContext(); + const { selectedPicker } = useAttachmentPickerState(); + + const setCommandsPicker = useStableCallback(() => { + attachmentPickerStore.setSelectedPicker('commands'); + }); + + return hasCommands ? ( + + ) : null; +}; diff --git a/package/src/components/AttachmentPicker/components/CameraSelectorIcon.tsx b/package/src/components/AttachmentPicker/components/CameraSelectorIcon.tsx deleted file mode 100644 index 6361cbac5e..0000000000 --- a/package/src/components/AttachmentPicker/components/CameraSelectorIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { Camera } from '../../../icons'; - -export const CameraSelectorIcon = () => { - const { - theme: { - colors: { grey }, - }, - } = useTheme(); - - return ; -}; diff --git a/package/src/components/AttachmentPicker/components/FileSelectorIcon.tsx b/package/src/components/AttachmentPicker/components/FileSelectorIcon.tsx deleted file mode 100644 index 1627088c7a..0000000000 --- a/package/src/components/AttachmentPicker/components/FileSelectorIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { Folder } from '../../../icons'; - -export const FileSelectorIcon = () => { - const { - theme: { - colors: { grey }, - }, - } = useTheme(); - - return ; -}; diff --git a/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx b/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx index 8d9308861c..8085efcbb7 100644 --- a/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx +++ b/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx @@ -2,11 +2,12 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { Check } from '../../../icons'; +import { primitives } from '../../../theme'; +import { BadgeNotification } from '../../ui'; const styles = StyleSheet.create({ check: { - borderRadius: 12, + borderRadius: primitives.radiusMax, height: 24, marginRight: 8, marginTop: 8, @@ -14,18 +15,26 @@ const styles = StyleSheet.create({ }, }); -export const ImageOverlaySelectedComponent = () => { +export const ImageOverlaySelectedComponent = ({ index }: { index: number }) => { const { theme: { + semantics, attachmentPicker: { imageOverlaySelectedComponent: { check }, }, - colors: { white }, }, } = useTheme(); return ( - - + + {index !== -1 ? : null} ); }; diff --git a/package/src/components/AttachmentPicker/components/ImageSelectorIcon.tsx b/package/src/components/AttachmentPicker/components/ImageSelectorIcon.tsx deleted file mode 100644 index bc9cce8603..0000000000 --- a/package/src/components/AttachmentPicker/components/ImageSelectorIcon.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; - -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { Picture } from '../../../icons'; - -type Props = { - selectedPicker?: 'images'; -}; - -export const ImageSelectorIcon = ({ selectedPicker }: Props) => { - const { - theme: { - colors: { accent_blue, grey }, - }, - } = useTheme(); - - return ; -}; diff --git a/package/src/components/AttachmentPicker/components/VideoRecorderSelectorIcon.tsx b/package/src/components/AttachmentPicker/components/VideoRecorderSelectorIcon.tsx deleted file mode 100644 index d1e675f854..0000000000 --- a/package/src/components/AttachmentPicker/components/VideoRecorderSelectorIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { Recorder } from '../../../icons'; - -export const VideoRecorderSelectorIcon = () => { - const { - theme: { - colors: { grey }, - }, - } = useTheme(); - - return ; -}; diff --git a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx index 3f492e9705..0cbfcf0ca6 100644 --- a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx +++ b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx @@ -9,24 +9,25 @@ import { Flag, GiphyIcon, Imgur, Lightning, Mute, Sound, UserAdd, UserDelete } f export const SuggestionCommandIcon = ({ name }: { name: CommandVariants }) => { const { theme: { + semantics, colors: { white }, }, } = useTheme(); if (name === 'ban') { - return ; + return ; } else if (name === 'flag') { - return ; + return ; } else if (name === 'giphy') { - return ; + return ; } else if (name === 'imgur') { - return ; + return ; } else if (name === 'mute') { - return ; + return ; } else if (name === 'unban') { - return ; + return ; } else if (name === 'unmute') { - return ; + return ; } else { return ; } @@ -35,7 +36,6 @@ export const SuggestionCommandIcon = ({ name }: { name: CommandVariants }) => { export const AutoCompleteSuggestionCommandIcon = ({ name }: { name: CommandVariants }) => { const { theme: { - colors: { accent_blue }, messageInput: { suggestions: { command: { iconContainer }, @@ -45,15 +45,7 @@ export const AutoCompleteSuggestionCommandIcon = ({ name }: { name: CommandVaria } = useTheme(); return ( - + ); diff --git a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx index 46175f0283..afc86d1c68 100644 --- a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx +++ b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { CommandSuggestion, TextComposerSuggestion, UserSuggestion } from 'stream-chat'; @@ -8,6 +8,7 @@ import { AutoCompleteSuggestionCommandIcon } from './AutoCompleteSuggestionComma import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { AtMentions } from '../../icons/AtMentions'; +import { primitives } from '../../theme'; import type { Emoji } from '../../types/types'; import { UserAvatar } from '../ui/Avatar/UserAvatar'; @@ -29,6 +30,8 @@ export const MentionSuggestionItem = (item: UserSuggestion) => { }, }, } = useTheme(); + const styles = useStyles(); + return ( @@ -54,6 +57,8 @@ export const EmojiSuggestionItem = (item: Emoji) => { }, }, } = useTheme(); + const styles = useStyles(); + return ( @@ -78,9 +83,10 @@ export const CommandSuggestionItem = (item: CommandSuggestion) => { }, }, } = useTheme(); + const styles = useStyles(); return ( - + {name ? : null} {(name || '').replace(/^\w/, (char) => char.toUpperCase())} @@ -167,36 +173,54 @@ export const AutoCompleteSuggestionItem = (props: AutoCompleteSuggestionItemProp ); -const styles = StyleSheet.create({ - args: { - fontSize: 14, - }, - column: { - flex: 1, - justifyContent: 'space-evenly', - paddingLeft: 8, - }, - container: { - alignItems: 'center', - flexDirection: 'row', - paddingHorizontal: 16, - paddingVertical: 8, - }, - name: { - fontSize: 14, - fontWeight: 'bold', - paddingBottom: 2, - }, - tag: { - fontSize: 12, - fontWeight: '600', - }, - text: { - fontSize: 14, - }, - title: { - fontSize: 14, - fontWeight: 'bold', - paddingHorizontal: 8, - }, -}); +const useStyles = () => { + const { + theme: { semantics }, + } = useTheme(); + + return useMemo( + () => + StyleSheet.create({ + args: { + fontSize: primitives.typographyFontSizeMd, + color: semantics.textTertiary, + }, + column: { + flex: 1, + justifyContent: 'space-evenly', + paddingLeft: 8, + }, + container: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 16, + paddingVertical: 8, + }, + commandContainer: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: primitives.spacingSm, + paddingVertical: primitives.spacingXs, + }, + name: { + fontSize: 14, + fontWeight: 'bold', + paddingBottom: 2, + }, + tag: { + fontSize: 12, + fontWeight: '600', + }, + text: { + fontSize: 14, + }, + title: { + fontSize: primitives.typographyFontSizeMd, + fontWeight: primitives.typographyFontWeightSemiBold, + color: semantics.textPrimary, + width: 80, + }, + }), + [semantics.textPrimary, semantics.textTertiary], + ); +}; diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 083cff7258..d56d26c20f 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -42,11 +42,6 @@ import { useCreateTypingContext } from './hooks/useCreateTypingContext'; import { useMessageListPagination } from './hooks/useMessageListPagination'; import { useTargetedMessage } from './hooks/useTargetedMessage'; -import { CameraSelectorIcon as DefaultCameraSelectorIcon } from '../../components/AttachmentPicker/components/CameraSelectorIcon'; -import { FileSelectorIcon as DefaultFileSelectorIcon } from '../../components/AttachmentPicker/components/FileSelectorIcon'; -import { ImageSelectorIcon as DefaultImageSelectorIcon } from '../../components/AttachmentPicker/components/ImageSelectorIcon'; -import { VideoRecorderSelectorIcon as DefaultVideoRecorderSelectorIcon } from '../../components/AttachmentPicker/components/VideoRecorderSelectorIcon'; -import { CreatePollIcon as DefaultCreatePollIcon } from '../../components/Poll/components/CreatePollIcon'; import { AttachmentPickerContextValue, AttachmentPickerProvider, @@ -88,7 +83,7 @@ import { useTranslationContext, } from '../../contexts/translationContext/TranslationContext'; import { TypingProvider } from '../../contexts/typingContext/TypingContext'; -import { useStableCallback, useViewport } from '../../hooks'; +import { useStableCallback } from '../../hooks'; import { useAppStateListener } from '../../hooks/useAppStateListener'; import { useAttachmentPickerBottomSheet } from '../../hooks/useAttachmentPickerBottomSheet'; @@ -134,11 +129,8 @@ import { ImageLoadingIndicator as ImageLoadingIndicatorDefault } from '../Attach import { ImageReloadIndicator as ImageReloadIndicatorDefault } from '../Attachment/ImageReloadIndicator'; import { URLPreview as URLPreviewDefault } from '../Attachment/UrlPreview'; import { VideoThumbnail as VideoThumbnailDefault } from '../Attachment/VideoThumbnail'; -import { AttachmentPicker, AttachmentPickerProps } from '../AttachmentPicker/AttachmentPicker'; -import { AttachmentPickerBottomSheetHandle as DefaultAttachmentPickerBottomSheetHandle } from '../AttachmentPicker/components/AttachmentPickerBottomSheetHandle'; -import { AttachmentPickerError as DefaultAttachmentPickerError } from '../AttachmentPicker/components/AttachmentPickerError'; -import { AttachmentPickerErrorImage as DefaultAttachmentPickerErrorImage } from '../AttachmentPicker/components/AttachmentPickerErrorImage'; -import { AttachmentPickerIOSSelectMorePhotos as DefaultAttachmentPickerIOSSelectMorePhotos } from '../AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos'; +import { AttachmentPicker } from '../AttachmentPicker/AttachmentPicker'; +import { AttachmentPickerContent as DefaultAttachmentPickerContent } from '../AttachmentPicker/components/AttachmentPickerContent'; import { AttachmentPickerSelectionBar as DefaultAttachmentPickerSelectionBar } from '../AttachmentPicker/components/AttachmentPickerSelectionBar'; import { ImageOverlaySelectedComponent as DefaultImageOverlaySelectedComponent } from '../AttachmentPicker/components/ImageOverlaySelectedComponent'; import { AutoCompleteSuggestionHeader as AutoCompleteSuggestionHeaderDefault } from '../AutoCompleteInput/AutoCompleteSuggestionHeader'; @@ -281,20 +273,19 @@ const debounceOptions = { }; export type ChannelPropsWithContext = Pick & - Partial< - Pick - > & Partial< Pick< - AttachmentPickerProps, - | 'AttachmentPickerError' - | 'AttachmentPickerErrorImage' - | 'AttachmentPickerIOSSelectMorePhotos' + AttachmentPickerContextValue, + | 'bottomInset' + | 'topInset' + | 'disableAttachmentPicker' | 'ImageOverlaySelectedComponent' + | 'numberOfAttachmentPickerImageColumns' + | 'AttachmentPickerIOSSelectMorePhotos' | 'attachmentPickerErrorButtonText' | 'attachmentPickerErrorText' | 'numberOfAttachmentImagesToLoadPerCall' - | 'numberOfAttachmentPickerImageColumns' + | 'AttachmentPickerContent' > > & Partial< @@ -549,8 +540,6 @@ export type ChannelPropsWithContext = Pick & >; const ChannelWithContext = (props: PropsWithChildren) => { - const { vh } = useViewport(); - const { additionalKeyboardAvoidingViewProps, additionalPressableProps, @@ -563,11 +552,9 @@ const ChannelWithContext = (props: PropsWithChildren) = asyncMessagesSlideToCancelDistance = 75, AttachButton = AttachButtonDefault, Attachment = AttachmentDefault, - AttachmentPickerBottomSheetHandle = DefaultAttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight = 20, - attachmentPickerBottomSheetHeight = vh(45), + attachmentPickerBottomSheetHeight = 333, AttachmentPickerSelectionBar = DefaultAttachmentPickerSelectionBar, - attachmentSelectionBarHeight = 52, + attachmentSelectionBarHeight = 72, AudioAttachment = AudioAttachmentDefault, AudioAttachmentUploadPreview = AudioAttachmentUploadPreviewDefault, AudioRecorder = AudioRecorderDefault, @@ -579,22 +566,14 @@ const ChannelWithContext = (props: PropsWithChildren) = AutoCompleteSuggestionHeader = AutoCompleteSuggestionHeaderDefault, AutoCompleteSuggestionItem = AutoCompleteSuggestionItemDefault, AutoCompleteSuggestionList = AutoCompleteSuggestionListDefault, - AttachmentPickerError = DefaultAttachmentPickerError, - AttachmentPickerErrorImage = DefaultAttachmentPickerErrorImage, - AttachmentPickerIOSSelectMorePhotos = DefaultAttachmentPickerIOSSelectMorePhotos, AttachmentUploadPreviewList = AttachmentUploadPreviewDefault, ImageOverlaySelectedComponent = DefaultImageOverlaySelectedComponent, attachmentPickerErrorButtonText, attachmentPickerErrorText, - numberOfAttachmentImagesToLoadPerCall = 60, + numberOfAttachmentImagesToLoadPerCall = 25, numberOfAttachmentPickerImageColumns = 3, giphyVersion = 'fixed_height', bottomInset = 0, - CameraSelectorIcon = DefaultCameraSelectorIcon, - FileSelectorIcon = DefaultFileSelectorIcon, - CreatePollIcon = DefaultCreatePollIcon, - ImageSelectorIcon = DefaultImageSelectorIcon, - VideoRecorderSelectorIcon = DefaultVideoRecorderSelectorIcon, channel, children, client, @@ -763,6 +742,7 @@ const ChannelWithContext = (props: PropsWithChildren) = isOnline, maximumMessageLimit, initializeOnMount = true, + AttachmentPickerContent = DefaultAttachmentPickerContent, } = props; const { thread: threadProps, threadInstance } = threadFromProps; @@ -1731,49 +1711,46 @@ const ChannelWithContext = (props: PropsWithChildren) = } }); - const attachmentPickerProps = useMemo( + const handleClosePicker = useStableCallback(() => closePicker(bottomSheetRef)); + const handleOpenPicker = useStableCallback(() => openPicker(bottomSheetRef)); + + const attachmentPickerContext = useMemo( () => ({ - AttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight, + bottomInset, + bottomSheetRef, + closePicker: handleClosePicker, + disableAttachmentPicker, + openPicker: handleOpenPicker, + topInset, + ImageOverlaySelectedComponent, + AttachmentPickerSelectionBar, + numberOfAttachmentPickerImageColumns, attachmentPickerBottomSheetHeight, - AttachmentPickerError, attachmentPickerErrorButtonText, - AttachmentPickerErrorImage, attachmentPickerErrorText, - AttachmentPickerIOSSelectMorePhotos, attachmentSelectionBarHeight, - ImageOverlaySelectedComponent, numberOfAttachmentImagesToLoadPerCall, - numberOfAttachmentPickerImageColumns, + AttachmentPickerContent, }), [ - AttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight, + bottomInset, + bottomSheetRef, + handleClosePicker, + disableAttachmentPicker, + handleOpenPicker, + topInset, + ImageOverlaySelectedComponent, + AttachmentPickerSelectionBar, + numberOfAttachmentPickerImageColumns, attachmentPickerBottomSheetHeight, - AttachmentPickerError, attachmentPickerErrorButtonText, - AttachmentPickerErrorImage, attachmentPickerErrorText, - AttachmentPickerIOSSelectMorePhotos, attachmentSelectionBarHeight, - ImageOverlaySelectedComponent, numberOfAttachmentImagesToLoadPerCall, - numberOfAttachmentPickerImageColumns, + AttachmentPickerContent, ], ); - const attachmentPickerContext = useMemo( - () => ({ - bottomInset, - bottomSheetRef, - closePicker: () => closePicker(bottomSheetRef), - disableAttachmentPicker, - openPicker: () => openPicker(bottomSheetRef), - topInset, - }), - [bottomInset, bottomSheetRef, closePicker, openPicker, topInset, disableAttachmentPicker], - ); - const ownCapabilitiesContext = useCreateOwnCapabilitiesContext({ channel, overrideCapabilities: overrideOwnCapabilities, @@ -1836,8 +1813,6 @@ const ChannelWithContext = (props: PropsWithChildren) = asyncMessagesMultiSendEnabled, asyncMessagesSlideToCancelDistance, AttachButton, - AttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight, attachmentPickerBottomSheetHeight, AttachmentPickerSelectionBar, attachmentSelectionBarHeight, @@ -1852,16 +1827,13 @@ const ChannelWithContext = (props: PropsWithChildren) = AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem, AutoCompleteSuggestionList, - CameraSelectorIcon, channelId, compressImageQuality, CooldownTimer, CreatePollContent, - CreatePollIcon, doFileUploadRequest, editMessage, FileAttachmentUploadPreview, - FileSelectorIcon, FileUploadInProgressIndicator, FileUploadRetryIndicator, FileUploadNotSupportedIndicator, @@ -1874,7 +1846,6 @@ const ChannelWithContext = (props: PropsWithChildren) = hasFilePicker, hasImagePicker, ImageAttachmentUploadPreview, - ImageSelectorIcon, Input, InputButtons, messageInputFloating, @@ -1888,7 +1859,6 @@ const ChannelWithContext = (props: PropsWithChildren) = StartAudioRecordingButton, StopMessageStreamingButton, VideoAttachmentUploadPreview, - VideoRecorderSelectorIcon, }); const messageListContext = useCreatePaginatedMessageListContext({ @@ -2074,9 +2044,7 @@ const ChannelWithContext = (props: PropsWithChildren) = {children} - {!disableAttachmentPicker && ( - - )} + {!disableAttachmentPicker ? : null} diff --git a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts index f6b07d5983..46a7001697 100644 --- a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts +++ b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts @@ -10,8 +10,6 @@ export const useCreateInputMessageInputContext = ({ asyncMessagesMultiSendEnabled, asyncMessagesSlideToCancelDistance, AttachButton, - AttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight, attachmentPickerBottomSheetHeight, AttachmentPickerSelectionBar, attachmentSelectionBarHeight, @@ -27,15 +25,12 @@ export const useCreateInputMessageInputContext = ({ AutoCompleteSuggestionItem, AutoCompleteSuggestionList, channelId, - CameraSelectorIcon, compressImageQuality, CooldownTimer, CreatePollContent, - CreatePollIcon, doFileUploadRequest, editMessage, FileAttachmentUploadPreview, - FileSelectorIcon, FileUploadInProgressIndicator, FileUploadRetryIndicator, FileUploadNotSupportedIndicator, @@ -48,7 +43,6 @@ export const useCreateInputMessageInputContext = ({ hasFilePicker, hasImagePicker, ImageAttachmentUploadPreview, - ImageSelectorIcon, Input, InputButtons, messageInputFloating, @@ -63,7 +57,6 @@ export const useCreateInputMessageInputContext = ({ StartAudioRecordingButton, StopMessageStreamingButton, VideoAttachmentUploadPreview, - VideoRecorderSelectorIcon, }: InputMessageInputContextValue & { /** * To ensure we allow re-render, when channel is changed @@ -79,8 +72,6 @@ export const useCreateInputMessageInputContext = ({ asyncMessagesMultiSendEnabled, asyncMessagesSlideToCancelDistance, AttachButton, - AttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight, attachmentPickerBottomSheetHeight, AttachmentPickerSelectionBar, attachmentSelectionBarHeight, @@ -95,15 +86,12 @@ export const useCreateInputMessageInputContext = ({ AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem, AutoCompleteSuggestionList, - CameraSelectorIcon, compressImageQuality, CooldownTimer, CreatePollContent, - CreatePollIcon, doFileUploadRequest, editMessage, FileAttachmentUploadPreview, - FileSelectorIcon, FileUploadInProgressIndicator, FileUploadRetryIndicator, FileUploadNotSupportedIndicator, @@ -116,7 +104,6 @@ export const useCreateInputMessageInputContext = ({ hasFilePicker, hasImagePicker, ImageAttachmentUploadPreview, - ImageSelectorIcon, Input, InputButtons, messageInputFloating, @@ -131,7 +118,6 @@ export const useCreateInputMessageInputContext = ({ StartAudioRecordingButton, StopMessageStreamingButton, VideoAttachmentUploadPreview, - VideoRecorderSelectorIcon, }), // eslint-disable-next-line react-hooks/exhaustive-deps [ diff --git a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap index f771d43d78..3d22f4c7f1 100644 --- a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap +++ b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap @@ -122,48 +122,473 @@ exports[`MessageStatus should render message status with read by container 1`] = - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `; diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx index ad5d2cff7e..976f83da18 100644 --- a/package/src/components/MessageInput/MessageInput.tsx +++ b/package/src/components/MessageInput/MessageInput.tsx @@ -25,11 +25,12 @@ import { MessageInputTrailingView } from './MessageInputTrailingView'; import { audioRecorderSelector } from './utils/audioRecorderSelectors'; -import { ChatContextValue, useChatContext, useOwnCapabilitiesContext } from '../../contexts'; import { - AttachmentPickerContextValue, + ChatContextValue, useAttachmentPickerContext, -} from '../../contexts/attachmentPickerContext/AttachmentPickerContext'; + useChatContext, + useOwnCapabilitiesContext, +} from '../../contexts'; import { ChannelContextValue, useChannelContext, @@ -54,6 +55,7 @@ import { useTranslationContext, } from '../../contexts/translationContext/TranslationContext'; +import { useAttachmentPickerState } from '../../hooks/useAttachmentPickerState'; import { useKeyboardVisibility } from '../../hooks/useKeyboardVisibility'; import { useStateStore } from '../../hooks/useStateStore'; import { AudioRecorderManagerState } from '../../state-store/audio-recorder-manager'; @@ -137,11 +139,7 @@ const useStyles = () => { }, [semantics]); }; -type MessageInputPropsWithContext = Pick< - AttachmentPickerContextValue, - 'bottomInset' | 'disableAttachmentPicker' | 'selectedPicker' -> & - Pick & +type MessageInputPropsWithContext = Pick & Pick & Pick< MessageInputContextValue, @@ -152,9 +150,6 @@ type MessageInputPropsWithContext = Pick< | 'asyncMessagesMinimumPressDuration' | 'asyncMessagesSlideToCancelDistance' | 'asyncMessagesMultiSendEnabled' - | 'attachmentPickerBottomSheetHeight' - | 'AttachmentPickerSelectionBar' - | 'attachmentSelectionBarHeight' | 'AttachmentUploadPreviewList' | 'AudioRecorder' | 'AudioRecordingInProgress' @@ -166,13 +161,8 @@ type MessageInputPropsWithContext = Pick< | 'Input' | 'inputBoxRef' | 'InputButtons' - | 'CameraSelectorIcon' - | 'CreatePollIcon' - | 'FileSelectorIcon' | 'messageInputFloating' | 'messageInputHeightStore' - | 'ImageSelectorIcon' - | 'VideoRecorderSelectorIcon' | 'SendButton' | 'ShowThreadMessageInChannelButton' | 'StartAudioRecordingButton' @@ -205,11 +195,6 @@ const messageInputHeightStoreSelector = (state: MessageInputHeightState) => ({ const MessageInputWithContext = (props: MessageInputPropsWithContext) => { const { - AttachmentPickerSelectionBar, - attachmentPickerBottomSheetHeight, - attachmentSelectionBarHeight, - bottomInset, - selectedPicker, additionalTextInputProps, asyncMessagesLockDistance, asyncMessagesSlideToCancelDistance, @@ -221,7 +206,6 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => { closeAttachmentPicker, closePollCreationDialog, CreatePollContent, - disableAttachmentPicker, editing, messageInputFloating, messageInputHeightStore, @@ -241,6 +225,8 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => { } = props; const styles = useStyles(); + const { selectedPicker } = useAttachmentPickerState(); + const { attachmentPickerBottomSheetHeight, bottomInset } = useAttachmentPickerContext(); const messageComposer = useMessageComposer(); const { height } = useStateStore(messageInputHeightStore.store, messageInputHeightStoreSelector); @@ -249,7 +235,6 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => { theme: { semantics, messageInput: { - attachmentSelectionBar, container, floatingWrapper, focusedInputBoxContainer, @@ -349,7 +334,7 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => { })); const { bottom } = useSafeAreaInsets(); - const BOTTOM_OFFSET = isKeyboardVisible ? 16 : bottom ? bottom : 16; + const BOTTOM_OFFSET = isKeyboardVisible || selectedPicker ? 16 : bottom ? bottom : 16; const micPositionContextValue = useMemo( () => ({ micPositionX, micPositionY }), @@ -358,146 +343,133 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => { return ( - <> - - messageInputHeightStore.setHeight( - messageInputFloating ? newHeight + BOTTOM_OFFSET : newHeight, - ) - } // BOTTOM OFFSET is the position of the input from the bottom of the screen - style={ - messageInputFloating - ? [styles.wrapper, styles.floatingWrapper, { bottom: BOTTOM_OFFSET }, floatingWrapper] - : [ - styles.wrapper, - { - borderTopWidth: 1, - backgroundColor: semantics.composerBg, - borderColor: semantics.borderCoreDefault, - paddingBottom: BOTTOM_OFFSET, - }, - wrapper, - ] - } - > - {Input ? ( - - ) : ( - - - - - {recordingStatus === 'stopped' ? ( - - ) : micLocked ? ( - - ) : null} - - - - - {!isRecordingStateIdle ? ( - - ) : ( - <> - - - - - )} - - - - - - - )} - - - - {!isRecordingStateIdle ? ( - - - + {/* TODO V9: Think of a better way to do this without so much re-layouting. */} + + messageInputHeightStore.setHeight( + messageInputFloating ? newHeight + BOTTOM_OFFSET : newHeight, + ) + } // BOTTOM OFFSET is the position of the input from the bottom of the screen + style={ + messageInputFloating + ? [styles.wrapper, styles.floatingWrapper, { bottom: BOTTOM_OFFSET }, floatingWrapper] + : [ + styles.wrapper, + { + borderTopWidth: 1, + backgroundColor: semantics.composerBg, + borderColor: semantics.borderCoreDefault, + // paddingBottom: BOTTOM_OFFSET, + paddingBottom: + selectedPicker && !isKeyboardVisible + ? attachmentPickerBottomSheetHeight - bottomInset + BOTTOM_OFFSET + : BOTTOM_OFFSET, + }, + wrapper, + ] + } + > + {Input ? ( + ) : ( - + + + + + {recordingStatus === 'stopped' ? ( + + ) : micLocked ? ( + + ) : null} + + + + + {!isRecordingStateIdle ? ( + + ) : ( + <> + + + + + )} + + + + + + )} - - + + + {!isRecordingStateIdle ? ( + - - - {!disableAttachmentPicker && selectedPicker ? ( - + + ) : ( + + )} + + + + + + {showPollCreationDialog ? ( + + - - - ) : null} - - {showPollCreationDialog ? ( - - - - - - - - - - ) : null} - + + + + + + + + ) : null} ); }; @@ -518,7 +490,6 @@ const areEqual = ( isKeyboardVisible: prevIsKeyboardVisible, isOnline: prevIsOnline, openPollCreationDialog: prevOpenPollCreationDialog, - selectedPicker: prevSelectedPicker, showPollCreationDialog: prevShowPollCreationDialog, t: prevT, threadList: prevThreadList, @@ -538,7 +509,6 @@ const areEqual = ( isKeyboardVisible: nextIsKeyboardVisible, isOnline: nextIsOnline, openPollCreationDialog: nextOpenPollCreationDialog, - selectedPicker: nextSelectedPicker, showPollCreationDialog: nextShowPollCreationDialog, t: nextT, threadList: nextThreadList, @@ -614,11 +584,6 @@ const areEqual = ( return false; } - const selectedPickerEqual = prevSelectedPicker === nextSelectedPicker; - if (!selectedPickerEqual) { - return false; - } - const micLockedEqual = prevMicLocked === nextMicLocked; if (!micLockedEqual) { return false; @@ -666,11 +631,6 @@ export const MessageInput = (props: MessageInputProps) => { asyncMessagesMinimumPressDuration, asyncMessagesMultiSendEnabled, asyncMessagesSlideToCancelDistance, - AttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight, - attachmentPickerBottomSheetHeight, - AttachmentPickerSelectionBar, - attachmentSelectionBarHeight, AttachmentUploadPreviewList, AudioRecorder, audioRecordingEnabled, @@ -679,14 +639,10 @@ export const MessageInput = (props: MessageInputProps) => { AudioRecordingPreview, AudioRecordingWaveform, AutoCompleteSuggestionList, - CameraSelectorIcon, closeAttachmentPicker, closePollCreationDialog, compressImageQuality, CreatePollContent, - CreatePollIcon, - FileSelectorIcon, - ImageSelectorIcon, Input, inputBoxRef, InputButtons, @@ -701,10 +657,7 @@ export const MessageInput = (props: MessageInputProps) => { StartAudioRecordingButton, StopMessageStreamingButton, uploadNewFile, - VideoRecorderSelectorIcon, } = useMessageInputContext(); - const { bottomInset, bottomSheetRef, disableAttachmentPicker, selectedPicker } = - useAttachmentPickerContext(); const messageComposer = useMessageComposer(); const editing = !!messageComposer.editedMessage; const { clearEditingState } = useMessageComposerAPIContext(); @@ -739,11 +692,6 @@ export const MessageInput = (props: MessageInputProps) => { asyncMessagesMinimumPressDuration, asyncMessagesMultiSendEnabled, asyncMessagesSlideToCancelDistance, - AttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight, - attachmentPickerBottomSheetHeight, - AttachmentPickerSelectionBar, - attachmentSelectionBarHeight, AttachmentUploadPreviewList, AudioRecorder, audioRecordingEnabled, @@ -752,20 +700,13 @@ export const MessageInput = (props: MessageInputProps) => { AudioRecordingPreview, AudioRecordingWaveform, AutoCompleteSuggestionList, - bottomInset, - bottomSheetRef, - CameraSelectorIcon, channel, clearEditingState, closeAttachmentPicker, closePollCreationDialog, compressImageQuality, CreatePollContent, - CreatePollIcon, - disableAttachmentPicker, editing, - FileSelectorIcon, - ImageSelectorIcon, Input, inputBoxRef, InputButtons, @@ -776,7 +717,6 @@ export const MessageInput = (props: MessageInputProps) => { messageInputHeightStore, openPollCreationDialog, Reply, - selectedPicker, SendButton, sendMessage, SendMessageDisallowedIndicator, @@ -787,7 +727,6 @@ export const MessageInput = (props: MessageInputProps) => { t, threadList, uploadNewFile, - VideoRecorderSelectorIcon, watchers, }} {...props} diff --git a/package/src/components/MessageInput/__tests__/AttachButton.test.js b/package/src/components/MessageInput/__tests__/AttachButton.test.js index dbd9ae7305..a4198f36f5 100644 --- a/package/src/components/MessageInput/__tests__/AttachButton.test.js +++ b/package/src/components/MessageInput/__tests__/AttachButton.test.js @@ -124,28 +124,29 @@ describe('AttachButton', () => { }); }); - it("should open native attachment picker when the media library isn't present", async () => { - jest.spyOn(NativeHandler, 'isImageMediaLibraryAvailable').mockImplementation(() => false); - - const channelProps = { channel }; - const props = {}; - - renderComponent({ channelProps, client, props }); - - const { queryByTestId } = screen; - - await waitFor(() => { - expect(queryByTestId('attach-button')).toBeTruthy(); - }); - - act(() => { - fireEvent.press(screen.getByTestId('attach-button')); - }); - - await waitFor(() => { - expect(queryByTestId('native-attachment-picker')).toBeTruthy(); - }); - }); + // TODO: Re-enable later + // it("should open native attachment picker when the media library isn't present", async () => { + // jest.spyOn(NativeHandler, 'isImageMediaLibraryAvailable').mockImplementation(() => false); + // + // const channelProps = { channel }; + // const props = {}; + // + // renderComponent({ channelProps, client, props }); + // + // const { queryByTestId } = screen; + // + // await waitFor(() => { + // expect(queryByTestId('attach-button')).toBeTruthy(); + // }); + // + // act(() => { + // fireEvent.press(screen.getByTestId('attach-button')); + // }); + // + // await waitFor(() => { + // expect(queryByTestId('native-attachment-picker')).toBeTruthy(); + // }); + // }); it('should open stream attachment picker when the media library is present', async () => { jest.spyOn(NativeHandler, 'isImageMediaLibraryAvailable').mockImplementation(() => true); diff --git a/package/src/components/MessageInput/__tests__/MessageInput.test.js b/package/src/components/MessageInput/__tests__/MessageInput.test.js index 8cea3bef40..57ff764466 100644 --- a/package/src/components/MessageInput/__tests__/MessageInput.test.js +++ b/package/src/components/MessageInput/__tests__/MessageInput.test.js @@ -9,30 +9,28 @@ import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvide import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels'; +import { AttachmentPickerStore } from '../../../state-store/attachment-picker-store'; +import { AttachmentPickerContent } from '../../AttachmentPicker/components/AttachmentPickerContent'; import { AttachmentPickerSelectionBar } from '../../AttachmentPicker/components/AttachmentPickerSelectionBar'; -import { CameraSelectorIcon } from '../../AttachmentPicker/components/CameraSelectorIcon'; -import { FileSelectorIcon } from '../../AttachmentPicker/components/FileSelectorIcon'; -import { ImageSelectorIcon } from '../../AttachmentPicker/components/ImageSelectorIcon'; import { Channel } from '../../Channel/Channel'; import { Chat } from '../../Chat/Chat'; -import { CreatePollIcon } from '../../Poll'; import { MessageInput } from '../MessageInput'; jest.spyOn(Alert, 'alert'); jest.spyOn(AttachmentPickerUtils, 'useAttachmentPickerContext').mockImplementation( - jest.fn(() => ({ - AttachmentPickerSelectionBar, - CameraSelectorIcon, - closePicker: jest.fn(), - CreatePollIcon, - FileSelectorIcon, - ImageSelectorIcon, - openPicker: jest.fn(), - selectedPicker: 'images', - setBottomInset: jest.fn(), - setSelectedPicker: jest.fn(), - setTopInset: jest.fn(), - })), + jest.fn(() => { + const attachmentPickerStore = new AttachmentPickerStore(); + attachmentPickerStore.setSelectedPicker('images'); + return { + AttachmentPickerSelectionBar, + AttachmentPickerContent, + closePicker: jest.fn(), + openPicker: jest.fn(), + setBottomInset: jest.fn(), + setTopInset: jest.fn(), + attachmentPickerStore, + }; + }), ); const renderComponent = ({ channelProps, client, props }) => { diff --git a/package/src/components/MessageInput/__tests__/SendMessageDisallowedIndicator.test.js b/package/src/components/MessageInput/__tests__/SendMessageDisallowedIndicator.test.js index c7613cd31e..4cfd83d62b 100644 --- a/package/src/components/MessageInput/__tests__/SendMessageDisallowedIndicator.test.js +++ b/package/src/components/MessageInput/__tests__/SendMessageDisallowedIndicator.test.js @@ -16,20 +16,28 @@ import { generateLocalFileUploadAttachmentData } from '../../../mock-builders/at import { generateMessage } from '../../../mock-builders/generator/message'; +import { AttachmentPickerStore } from '../../../state-store/attachment-picker-store'; +import { AttachmentPickerContent } from '../../AttachmentPicker/components/AttachmentPickerContent'; +import { AttachmentPickerSelectionBar } from '../../AttachmentPicker/components/AttachmentPickerSelectionBar'; import { Channel } from '../../Channel/Channel'; import { Chat } from '../../Chat/Chat'; import { MessageInput } from '../MessageInput'; jest.spyOn(Alert, 'alert'); jest.spyOn(AttachmentPickerUtils, 'useAttachmentPickerContext').mockImplementation( - jest.fn(() => ({ - closePicker: jest.fn(), - openPicker: jest.fn(), - selectedPicker: 'images', - setBottomInset: jest.fn(), - setSelectedPicker: jest.fn(), - setTopInset: jest.fn(), - })), + jest.fn(() => { + const attachmentPickerStore = new AttachmentPickerStore(); + attachmentPickerStore.setSelectedPicker('images'); + return { + AttachmentPickerSelectionBar, + AttachmentPickerContent, + closePicker: jest.fn(), + openPicker: jest.fn(), + setBottomInset: jest.fn(), + setTopInset: jest.fn(), + attachmentPickerStore, + }; + }), ); const renderComponent = ({ channelProps, client, props }) => { diff --git a/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap b/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap index 99fa039df3..c90a05afb9 100644 --- a/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap +++ b/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap @@ -19,7 +19,6 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli } > - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + > = UploadAttachmentPreviewProps>; @@ -21,13 +21,6 @@ export const VideoAttachmentUploadPreview = ({ handleRetry, removeAttachments, }: VideoAttachmentUploadPreviewProps) => { - const styles = useStyles(); - - const durationLabel = useMemo( - () => (attachment.duration ? formatMsToMinSec(attachment.duration) : undefined), - [attachment.duration], - ); - return attachment.localMetadata.previewUri ? ( <> - {durationLabel ? ( - - - {durationLabel} - - ) : null} + ) : ( { + const styles = useStyles(); + const { + theme: { semantics }, + } = useTheme(); + + const durationLabel = useMemo(() => { + if (!duration) { + return undefined; + } + + if (format === 'timer') { + return getDurationLabelFromDuration(duration); + } + + return formatMsToMinSec(duration); + }, [duration, format]); + + return durationLabel ? ( + + + {durationLabel} + + ) : null; +}; + const useStyles = () => { const { theme: { diff --git a/package/src/components/MessageInput/components/InputButtons/AttachButton.tsx b/package/src/components/MessageInput/components/InputButtons/AttachButton.tsx index cce437b1e1..713b08bac6 100644 --- a/package/src/components/MessageInput/components/InputButtons/AttachButton.tsx +++ b/package/src/components/MessageInput/components/InputButtons/AttachButton.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import type { GestureResponderEvent, LayoutChangeEvent, LayoutRectangle } from 'react-native'; +import React from 'react'; +import type { GestureResponderEvent } from 'react-native'; import { AttachmentPickerContextValue, @@ -9,52 +9,26 @@ import { MessageInputContextValue, useMessageInputContext, } from '../../../../contexts/messageInputContext/MessageInputContext'; +import { useStableCallback } from '../../../../hooks'; import { NewPlus } from '../../../../icons/NewPlus'; import { Button } from '../../../ui/'; -import { NativeAttachmentPicker } from '../NativeAttachmentPicker'; -type AttachButtonPropsWithContext = Pick< - MessageInputContextValue, - 'handleAttachButtonPress' | 'toggleAttachmentPicker' -> & - Pick & { +type AttachButtonPropsWithContext = Pick & + Pick & { disabled?: boolean; /** Function that opens attachment options bottom sheet */ handleOnPress?: ((event: GestureResponderEvent) => void) & (() => void); - }; + } & { toggleAttachmentPicker: () => void }; const AttachButtonWithContext = (props: AttachButtonPropsWithContext) => { - const [showAttachButtonPicker, setShowAttachButtonPicker] = useState(false); - const [attachButtonLayoutRectangle, setAttachButtonLayoutRectangle] = useState(); const { disableAttachmentPicker, disabled = false, handleAttachButtonPress, handleOnPress, - selectedPicker, toggleAttachmentPicker, } = props; - const onAttachButtonLayout = (event: LayoutChangeEvent) => { - const layout = event.nativeEvent.layout; - setAttachButtonLayoutRectangle((prev) => { - if ( - prev && - prev.width === layout.width && - prev.height === layout.height && - prev.x === layout.x && - prev.y === layout.y - ) { - return prev; - } - return layout; - }); - }; - - const attachButtonHandler = () => { - setShowAttachButtonPicker((prevShowAttachButtonPicker) => !prevShowAttachButtonPicker); - }; - const onPressHandler = () => { if (disabled) { return; @@ -69,32 +43,20 @@ const AttachButtonWithContext = (props: AttachButtonPropsWithContext) => { } if (!disableAttachmentPicker) { toggleAttachmentPicker(); - } else { - attachButtonHandler(); } }; return ( - <> - - {showAttachButtonPicker ? ( - setShowAttachButtonPicker(false)} - /> - ) : null} - + ); }; @@ -102,19 +64,14 @@ const areEqual = ( prevProps: AttachButtonPropsWithContext, nextProps: AttachButtonPropsWithContext, ) => { - const { handleOnPress: prevHandleOnPress, selectedPicker: prevSelectedPicker } = prevProps; - const { handleOnPress: nextHandleOnPress, selectedPicker: nextSelectedPicker } = nextProps; + const { handleOnPress: prevHandleOnPress } = prevProps; + const { handleOnPress: nextHandleOnPress } = nextProps; const handleOnPressEqual = prevHandleOnPress === nextHandleOnPress; if (!handleOnPressEqual) { return false; } - const selectedPickerEqual = prevSelectedPicker === nextSelectedPicker; - if (!selectedPickerEqual) { - return false; - } - return true; }; @@ -129,15 +86,23 @@ export type AttachButtonProps = Partial; * UI Component for attach button in MessageInput component. */ export const AttachButton = (props: AttachButtonProps) => { - const { disableAttachmentPicker, selectedPicker } = useAttachmentPickerContext(); - const { handleAttachButtonPress, toggleAttachmentPicker } = useMessageInputContext(); + const { disableAttachmentPicker } = useAttachmentPickerContext(); + const { inputBoxRef, handleAttachButtonPress, openAttachmentPicker } = useMessageInputContext(); + const { attachmentPickerStore } = useAttachmentPickerContext(); + + const toggleAttachmentPicker = useStableCallback(() => { + if (attachmentPickerStore.state.getLatestValue().selectedPicker) { + inputBoxRef.current?.focus(); + } else { + openAttachmentPicker(); + } + }); return ( ; export type InputButtonsWithContextProps = Pick< MessageInputContextValue, - | 'AttachButton' - | 'hasCameraPicker' - | 'hasCommands' - | 'hasFilePicker' - | 'hasImagePicker' - | 'toggleAttachmentPicker' + 'AttachButton' | 'hasCameraPicker' | 'hasCommands' | 'hasFilePicker' | 'hasImagePicker' > & - Pick & Pick; export const InputButtonsWithContext = (props: InputButtonsWithContextProps) => { @@ -38,6 +35,8 @@ export const InputButtonsWithContext = (props: InputButtonsWithContextProps) => hasImagePicker, uploadFile: ownCapabilitiesUploadFile, } = props; + const { selectedPicker } = useAttachmentPickerState(); + const rotation = useSharedValue(0); const { theme: { @@ -45,6 +44,14 @@ export const InputButtonsWithContext = (props: InputButtonsWithContextProps) => }, } = useTheme(); + useEffect(() => { + rotation.value = withTiming(selectedPicker !== undefined ? 45 : 0, { duration: 200 }); + }, [selectedPicker, rotation]); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${rotation.value}deg` }], + })); + const hasAttachmentUploadCapabilities = (hasCameraPicker || hasFilePicker || hasImagePicker) && ownCapabilitiesUploadFile; @@ -56,7 +63,7 @@ export const InputButtonsWithContext = (props: InputButtonsWithContextProps) => @@ -71,14 +78,12 @@ const areEqual = ( hasCameraPicker: prevHasCameraPicker, hasFilePicker: prevHasFilePicker, hasImagePicker: prevHasImagePicker, - selectedPicker: prevSelectedPicker, } = prevProps; const { hasCameraPicker: nextHasCameraPicker, hasFilePicker: nextHasFilePicker, hasImagePicker: nextHasImagePicker, - selectedPicker: nextSelectedPicker, } = nextProps; if (prevHasCameraPicker !== nextHasCameraPicker) { @@ -93,10 +98,6 @@ const areEqual = ( return false; } - if (prevSelectedPicker !== nextSelectedPicker) { - return false; - } - return true; }; @@ -106,15 +107,8 @@ const MemoizedInputButtonsWithContext = React.memo( ) as typeof InputButtonsWithContext; export const InputButtons = (props: InputButtonsProps) => { - const { - AttachButton, - hasCameraPicker, - hasCommands, - hasFilePicker, - hasImagePicker, - toggleAttachmentPicker, - } = useMessageInputContext(); - const { selectedPicker } = useAttachmentPickerContext(); + const { AttachButton, hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker } = + useMessageInputContext(); const { uploadFile } = useOwnCapabilitiesContext(); return ( @@ -125,8 +119,6 @@ export const InputButtons = (props: InputButtonsProps) => { hasCommands, hasFilePicker, hasImagePicker, - selectedPicker, - toggleAttachmentPicker, uploadFile, }} {...props} diff --git a/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx b/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx index 3eaab0aca3..be1234740d 100644 --- a/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx +++ b/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx @@ -1,225 +1,12 @@ -import React, { useEffect, useRef } from 'react'; -import { Animated, Easing, LayoutRectangle, Platform, Pressable, StyleSheet } from 'react-native'; - -import { - useChannelContext, - useMessagesContext, - useOwnCapabilitiesContext, -} from '../../../contexts'; -import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext'; -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; - -import { CreatePollIcon } from '../../Poll/components/CreatePollIcon'; +import { LayoutRectangle } from 'react-native'; type NativeAttachmentPickerProps = { onRequestedClose: () => void; attachButtonLayoutRectangle?: LayoutRectangle; }; -const TOP_PADDING = 4; -const ATTACH_MARGIN_BOTTOM = 4; - -export const NativeAttachmentPicker = ({ - attachButtonLayoutRectangle, - onRequestedClose, -}: NativeAttachmentPickerProps) => { - const size = attachButtonLayoutRectangle?.width ?? 0; - const attachButtonItemSize = 40; - const { - theme: { - colors: { grey_whisper }, - messageInput: { - nativeAttachmentPicker: { - buttonContainer, - buttonDimmerStyle: buttonDimmerStyleTheme, - container, - }, - }, - }, - } = useTheme(); - const { - CameraSelectorIcon, - FileSelectorIcon, - ImageSelectorIcon, - VideoRecorderSelectorIcon, - hasCameraPicker, - hasFilePicker, - hasImagePicker, - openPollCreationDialog, - pickAndUploadImageFromNativePicker, - pickFile, - sendMessage, - takeAndUploadImage, - } = useMessageInputContext(); - const { threadList } = useChannelContext(); - const { hasCreatePoll } = useMessagesContext(); - const ownCapabilities = useOwnCapabilitiesContext(); - - const popupHeight = - // the top padding - TOP_PADDING + - // take margins into account - ATTACH_MARGIN_BOTTOM + - // the size of the attachment icon items (same size as attach button * amount of attachment button types) - attachButtonItemSize; - - const containerPopupStyle = { - borderTopEndRadius: size / 2, - // the popup should be rounded as the attach button - borderTopStartRadius: size / 2, - height: popupHeight, - // from the same side horizontal coordinate of the attach button - left: attachButtonLayoutRectangle?.x, - // we should show the popup right above the attach button and not top of it - top: (attachButtonLayoutRectangle?.y ?? 0) - popupHeight, - // the width of the popup should be the same as the attach button - width: size, - }; - - const elasticAnimRef = useRef(new Animated.Value(0.5)); // Initial value for scale: 0.5 - - useEffect(() => { - Animated.timing(elasticAnimRef.current, { - duration: 150, - easing: Easing.linear, - toValue: 1, - useNativeDriver: true, - }).start(); - }, []); - - const buttonStyle = { - borderRadius: attachButtonItemSize / 2, - height: attachButtonItemSize, - width: attachButtonItemSize, - }; - - const buttonDimmerStyle = { - ...styles.attachButtonDimmer, - height: size, - // from the same side horizontal coordinate of the attach button - left: attachButtonLayoutRectangle?.x, - // we should show the popup right on top of the attach button - top: attachButtonLayoutRectangle?.y ?? 0 - popupHeight + size, - width: size, - }; - - const onClose = ({ - onPressHandler, - }: { - onPressHandler?: (() => Promise) | (() => void); - }) => { - if (onPressHandler) { - onPressHandler(); - } - Animated.timing(elasticAnimRef.current, { - duration: 150, - easing: Easing.linear, - toValue: 0.2, - useNativeDriver: true, - }).start(onRequestedClose); - }; - - // do not allow poll creation in threads - const buttons = - !threadList && hasCreatePoll && ownCapabilities.sendPoll - ? [ - { - icon: , - id: 'Poll', - onPressHandler: () => { - openPollCreationDialog?.({ sendMessage }); - }, - }, - ] - : []; - - if (hasImagePicker) { - buttons.push({ - icon: , - id: 'Image', - onPressHandler: pickAndUploadImageFromNativePicker, - }); - } - if (hasFilePicker) { - buttons.push({ icon: , id: 'File', onPressHandler: pickFile }); - } - if (hasCameraPicker) { - buttons.push({ - icon: , - id: 'Camera', - onPressHandler: () => { - takeAndUploadImage(Platform.OS === 'android' ? 'image' : 'mixed'); - }, - }); - if (Platform.OS === 'android') { - buttons.push({ - icon: , - id: 'Video', - onPressHandler: () => { - takeAndUploadImage('video'); - }, - }); - } - } - - return ( - <> - { - onClose({}); - }} - style={[styles.container, containerPopupStyle, container]} - testID={'native-attachment-picker'} - > - {/* all the attach buttons */} - {buttons.map(({ icon, id, onPressHandler }) => ( - onClose({ onPressHandler })}> - - {icon} - - - ))} - - {/* a square view with 50% opacity that semi hides the attach button */} - onClose({})} style={[buttonDimmerStyle, buttonDimmerStyleTheme]} /> - - ); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const NativeAttachmentPicker = (_props: NativeAttachmentPickerProps) => { + // TODO: V9: Temporarily removed, will delete the entire component later + return null; }; - -const styles = StyleSheet.create({ - attachButtonDimmer: { - opacity: 0, - position: 'absolute', - }, - buttonContainer: { - alignItems: 'center', - justifyContent: 'center', - marginBottom: ATTACH_MARGIN_BOTTOM, - }, - container: { - alignItems: 'center', - justifyContent: 'flex-end', - paddingTop: TOP_PADDING, - position: 'absolute', - }, -}); diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx index b245ff7006..f5e90014d5 100644 --- a/package/src/components/MessageList/MessageFlashList.tsx +++ b/package/src/components/MessageList/MessageFlashList.tsx @@ -104,7 +104,7 @@ const messageInputHeightStoreSelector = (state: MessageInputHeightState) => ({ type MessageFlashListPropsWithContext = Pick< AttachmentPickerContextValue, - 'closePicker' | 'selectedPicker' | 'setSelectedPicker' + 'closePicker' | 'attachmentPickerStore' > & Pick & Pick< @@ -262,6 +262,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => ? InlineLoadingMoreRecentThreadIndicator : InlineLoadingMoreRecentIndicator; const { + attachmentPickerStore, additionalFlashListProps, channel, channelUnreadStateStore, @@ -295,10 +296,8 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => onThreadSelect, reloadChannel, ScrollToBottomButton, - selectedPicker, setChannelUnreadState, setFlatListRef, - setSelectedPicker, setTargetedMessage, StickyHeader, targetedMessage, @@ -667,7 +666,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => !viewableItems.length || !readEvents || lastReadMessageVisible || - selectedPicker === 'images' + attachmentPickerStore.state.getLatestValue().selectedPicker === 'images' ) { setIsUnreadNotificationOpen(false); return; @@ -938,19 +937,19 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => }); const dismissImagePicker = useStableCallback(() => { - if (selectedPicker) { - setSelectedPicker(undefined); + if (attachmentPickerStore.state.getLatestValue().selectedPicker) { + attachmentPickerStore.setSelectedPicker(undefined); closePicker(); } }); const onScrollBeginDrag: ScrollViewProps['onScrollBeginDrag'] = useStableCallback((event) => { - !hasMoved && selectedPicker && setHasMoved(true); + !hasMoved && attachmentPickerStore.state.getLatestValue().selectedPicker && setHasMoved(true); onUserScrollEvent(event); }); const onScrollEndDrag: ScrollViewProps['onScrollEndDrag'] = useStableCallback((event) => { - hasMoved && selectedPicker && setHasMoved(false); + hasMoved && attachmentPickerStore.state.getLatestValue().selectedPicker && setHasMoved(false); onUserScrollEvent(event); }); @@ -1008,8 +1007,8 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => } const changedBy = currentListHeightRef.current - height; - flashListRef.current?.scrollToOffset({ - offset: currentScrollOffsetRef.current + changedBy, + flashListRef.current?.getNativeScrollRef()?.setNativeProps({ + contentOffset: { x: 0, y: flashListRef.current?.getAbsoluteLastScrollOffset() + changedBy }, }); currentListHeightRef.current = height; }); @@ -1116,7 +1115,7 @@ export type MessageFlashListProps = Partial; * Please feel free to report any issues or suggestions. */ export const MessageFlashList = (props: MessageFlashListProps) => { - const { closePicker, selectedPicker, setSelectedPicker } = useAttachmentPickerContext(); + const { closePicker, attachmentPickerStore } = useAttachmentPickerContext(); const { channel, channelUnreadStateStore, @@ -1165,6 +1164,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => { return ( { reloadChannel, ScrollToBottomButton, scrollToFirstUnreadThreshold, - selectedPicker, setChannelUnreadState, - setSelectedPicker, setTargetedMessage, shouldShowUnreadUnderlay, StickyHeader, diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index b614133134..755a1134cb 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -135,7 +135,7 @@ const getPreviousLastMessage = (messages: LocalMessage[], newMessage?: MessageRe type MessageListPropsWithContext = Pick< AttachmentPickerContextValue, - 'closePicker' | 'selectedPicker' | 'setSelectedPicker' + 'closePicker' | 'attachmentPickerStore' > & Pick & Pick< @@ -265,6 +265,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { ? InlineLoadingMoreRecentThreadIndicator : InlineLoadingMoreRecentIndicator; const { + attachmentPickerStore, additionalFlatListProps, channel, channelUnreadStateStore, @@ -299,10 +300,8 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { readEvents, reloadChannel, ScrollToBottomButton, - selectedPicker, setChannelUnreadState, setFlatListRef, - setSelectedPicker, setTargetedMessage, StickyHeader, targetedMessage, @@ -481,7 +480,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { !viewableItems.length || !readEvents || lastReadMessageVisible || - selectedPicker === 'images' + attachmentPickerStore.state.getLatestValue().selectedPicker === 'images' ) { setIsUnreadNotificationOpen(false); return; @@ -1036,19 +1035,19 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { ); const dismissImagePicker = useStableCallback(() => { - if (selectedPicker) { - setSelectedPicker(undefined); + if (attachmentPickerStore.state.getLatestValue().selectedPicker) { + attachmentPickerStore.setSelectedPicker(undefined); closePicker(); } }); const onScrollBeginDrag: ScrollViewProps['onScrollBeginDrag'] = useStableCallback((event) => { - !hasMoved && selectedPicker && setHasMoved(true); + !hasMoved && attachmentPickerStore.state.getLatestValue().selectedPicker && setHasMoved(true); onUserScrollEvent(event); }); const onScrollEndDrag: ScrollViewProps['onScrollEndDrag'] = useStableCallback((event) => { - hasMoved && selectedPicker && setHasMoved(false); + hasMoved && attachmentPickerStore.state.getLatestValue().selectedPicker && setHasMoved(false); onUserScrollEvent(event); }); @@ -1218,7 +1217,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => { export type MessageListProps = Partial; export const MessageList = (props: MessageListProps) => { - const { closePicker, selectedPicker, setSelectedPicker } = useAttachmentPickerContext(); + const { closePicker, attachmentPickerStore } = useAttachmentPickerContext(); const { channel, channelUnreadStateStore, @@ -1266,6 +1265,7 @@ export const MessageList = (props: MessageListProps) => { return ( { reloadChannel, ScrollToBottomButton, scrollToFirstUnreadThreshold, - selectedPicker, setChannelUnreadState, - setSelectedPicker, setTargetedMessage, shouldShowUnreadUnderlay, StickyHeader, diff --git a/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap b/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap index e0d306f6eb..2143607093 100644 --- a/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap +++ b/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap @@ -83,6 +83,7 @@ exports[`ScrollToBottomButton should render the message notification and match s [ { "backgroundColor": "transparent", + "paddingHorizontal": 16, }, { "alignItems": "center", @@ -92,9 +93,6 @@ exports[`ScrollToBottomButton should render the message notification and match s "gap": 8, "justifyContent": "center", }, - { - "paddingHorizontal": 16, - }, ] } testID="scroll-to-bottom-button" diff --git a/package/src/components/Poll/components/CreatePollIcon.tsx b/package/src/components/Poll/components/CreatePollIcon.tsx deleted file mode 100644 index 40b1a23f98..0000000000 --- a/package/src/components/Poll/components/CreatePollIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { PollThumbnail } from '../../../icons'; - -export const CreatePollIcon = () => { - const { - theme: { - colors: { grey }, - }, - } = useTheme(); - - return ; -}; diff --git a/package/src/components/Poll/components/index.ts b/package/src/components/Poll/components/index.ts index 75f1df9c5f..4e917d632c 100644 --- a/package/src/components/Poll/components/index.ts +++ b/package/src/components/Poll/components/index.ts @@ -1,6 +1,5 @@ export * from './Button'; export * from './PollButtons'; -export * from './CreatePollIcon'; export * from './CreatePollOptions'; export * from './PollAnswersList'; export * from './PollInputDialog'; diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap index e51202cccb..fc2aa3d0ee 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -1926,11 +1926,17 @@ exports[`Thread should match thread snapshot 1`] = ` [ {}, {}, + { + "transform": [ + { + "rotate": "0deg", + }, + ], + }, ] } > - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `; diff --git a/package/src/components/index.ts b/package/src/components/index.ts index b482a0461c..891a913673 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -12,15 +12,11 @@ export * from './Attachment/UrlPreview'; export * from './Attachment/utils/buildGallery/buildGallery'; export * from './AttachmentPicker/AttachmentPicker'; -export * from './AttachmentPicker/components/AttachmentPickerBottomSheetHandle'; -export * from './AttachmentPicker/components/AttachmentPickerError'; -export * from './AttachmentPicker/components/AttachmentPickerErrorImage'; +export * from './AttachmentPicker/components/AttachmentPickerContent'; +export * from './AttachmentPicker/components/AttachmentMediaPicker'; export * from './AttachmentPicker/components/AttachmentPickerSelectionBar'; -export * from './AttachmentPicker/components/CameraSelectorIcon'; -export * from './AttachmentPicker/components/FileSelectorIcon'; -export * from './AttachmentPicker/components/VideoRecorderSelectorIcon'; +export * from './AttachmentPicker/components/AttachmentTypePickerButton'; export * from './AttachmentPicker/components/ImageOverlaySelectedComponent'; -export * from './AttachmentPicker/components/ImageSelectorIcon'; export * from './AutoCompleteInput/AutoCompleteInput'; export * from './AutoCompleteInput/AutoCompleteSuggestionHeader'; diff --git a/package/src/components/ui/Badge/BadgeNotification.tsx b/package/src/components/ui/Badge/BadgeNotification.tsx index 3c2a66c134..0ca3acdb05 100644 --- a/package/src/components/ui/Badge/BadgeNotification.tsx +++ b/package/src/components/ui/Badge/BadgeNotification.tsx @@ -7,11 +7,16 @@ import { primitives } from '../../../theme'; export type BadgeNotificationProps = { type: 'primary' | 'error' | 'neutral'; count: number; - size: 'sm' | 'md'; + size: 'sm' | 'md' | 'lg'; testID?: string; }; const sizes = { + lg: { + height: 24, + minWidth: 24, + borderWidth: 2, + }, md: { height: 20, minWidth: 20, @@ -25,6 +30,11 @@ const sizes = { }; const textStyles = { + lg: { + fontSize: primitives.typographyFontSizeSm, + fontWeight: primitives.typographyFontWeightBold, + lineHeight: 14, + }, md: { fontSize: primitives.typographyFontSizeSm, fontWeight: primitives.typographyFontWeightBold, diff --git a/package/src/components/ui/Button/Button.tsx b/package/src/components/ui/Button/Button.tsx index 24b7e64174..a52e34d8e5 100644 --- a/package/src/components/ui/Button/Button.tsx +++ b/package/src/components/ui/Button/Button.tsx @@ -95,7 +95,7 @@ export const Button = ({ borderWidth: buttonStyles.borderColor ? 1 : undefined, borderColor: disabled ? buttonStyles.disabledBorderColor : buttonStyles.borderColor, height: buttonSizes[size].height, - width: iconOnly ? buttonSizes[size].width : undefined, + width: iconOnly ? buttonSizes[size].width : '100%', }, style, ]} @@ -109,9 +109,9 @@ export const Button = ({ : selected ? semantics.backgroundCoreSelected : 'transparent', + paddingHorizontal: buttonPadding[size], }, styles.container, - { paddingHorizontal: buttonPadding[size] }, ]} disabled={disabled} {...rest} diff --git a/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx b/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx index 3463e882c6..7c7b2a9e5e 100644 --- a/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx +++ b/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx @@ -1,17 +1,30 @@ -import React, { PropsWithChildren, useContext, useEffect, useState } from 'react'; +import React, { PropsWithChildren, useContext, useMemo, useState } from 'react'; import BottomSheet from '@gorhom/bottom-sheet'; +import { AttachmentPickerContentProps } from '../../components'; +import { + AttachmentPickerStore, + SelectedPickerType, +} from '../../state-store/attachment-picker-store'; +import { MessageInputContextValue } from '../messageInputContext/MessageInputContext'; import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue'; import { isTestEnvironment } from '../utils/isTestEnvironment'; export type AttachmentPickerIconProps = { numberOfImageUploads?: number; - selectedPicker?: 'images'; + selectedPicker: SelectedPickerType; }; -export type AttachmentPickerContextValue = { +export type AttachmentPickerContextValue = Pick< + MessageInputContextValue, + 'attachmentSelectionBarHeight' | 'attachmentPickerBottomSheetHeight' +> & { + /** + * Custom UI Component to render select more photos for selected gallery access in iOS. + */ + AttachmentPickerIOSSelectMorePhotos: React.ComponentType; /** * `bottomInset` determine the height of the `AttachmentPicker` and the underlying shift to the `MessageList` when it is opened. * This can also be set via the `setBottomInset` function provided by the `useAttachmentPickerContext` hook. @@ -23,13 +36,24 @@ export type AttachmentPickerContextValue = { bottomSheetRef: React.RefObject; closePicker: () => void; openPicker: () => void; - setBottomInset: React.Dispatch>; - setSelectedPicker: React.Dispatch>; - setTopInset: React.Dispatch>; topInset: number; - selectedPicker?: 'images'; disableAttachmentPicker?: boolean; + /** + * Custom UI component to render overlay component, that shows up on top of [selected + * image](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/1.png) (with tick mark) + * + * **Default** + * [ImageOverlaySelectedComponent](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx) + */ + ImageOverlaySelectedComponent: React.ComponentType<{ index: number }>; + AttachmentPickerSelectionBar: React.ComponentType; + AttachmentPickerContent: React.ComponentType; + attachmentPickerStore: AttachmentPickerStore; + numberOfAttachmentPickerImageColumns?: number; + attachmentPickerErrorButtonText?: string; + attachmentPickerErrorText?: string; + numberOfAttachmentImagesToLoadPerCall?: number; }; export const AttachmentPickerContext = React.createContext( @@ -43,30 +67,18 @@ export const AttachmentPickerProvider = ({ value?: Pick & Partial>; }>) => { - const bottomInsetValue = value?.bottomInset; - const topInsetValue = value?.topInset; - - const [bottomInset, setBottomInset] = useState(bottomInsetValue ?? 0); - const [selectedPicker, setSelectedPicker] = useState<'images'>(); - const [topInset, setTopInset] = useState(topInsetValue ?? 0); - - useEffect(() => { - setBottomInset(bottomInsetValue ?? 0); - }, [bottomInsetValue]); - - useEffect(() => { - setTopInset(topInsetValue ?? 0); - }, [topInsetValue]); - - const combinedValue = { - selectedPicker, - setBottomInset, - setSelectedPicker, - setTopInset, - ...value, - bottomInset, - topInset, - }; + const { bottomInset = 0, topInset = 0, ...rest } = value ?? {}; + const [attachmentPickerStore] = useState(() => new AttachmentPickerStore()); + + const combinedValue = useMemo( + () => ({ + bottomInset, + topInset, + attachmentPickerStore, + ...rest, + }), + [bottomInset, topInset, attachmentPickerStore, rest], + ); return ( Promise; pickFile: () => Promise; - selectedPicker?: 'images'; sendMessage: () => Promise; /** * Ref callback to set reference on input box @@ -95,8 +89,9 @@ export type LocalMessageInputContext = { /** * Function for taking a photo and uploading it */ - takeAndUploadImage: (mediaType?: MediaTypes) => Promise; - toggleAttachmentPicker: () => void; + takeAndUploadImage: ( + mediaType?: MediaTypes, + ) => Promise<{ askToOpenSettings?: boolean; canceled?: boolean } | undefined>; uploadNewFile: (file: File) => Promise; audioRecorderManager: AudioRecorderManager; startVoiceRecording: () => Promise; @@ -176,20 +171,6 @@ export type InputMessageInputContextValue = { AutoCompleteSuggestionHeader: React.ComponentType; AutoCompleteSuggestionItem: React.ComponentType; AutoCompleteSuggestionList: React.ComponentType; - - /** - * Custom UI component to render [draggable handle](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/1.png) of attachmentpicker. - * - * **Default** - * [AttachmentPickerBottomSheetHandle](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/AttachmentPickerBottomSheetHandle.tsx) - */ - AttachmentPickerBottomSheetHandle: React.FC; - /** - * Height of the image picker bottom sheet handle. - * @type number - * @default 20 - */ - attachmentPickerBottomSheetHandleHeight: number; /** * Height of the image picker bottom sheet when opened. * @type number @@ -207,45 +188,11 @@ export type InputMessageInputContextValue = { * Height of the attachment selection bar displayed on the attachment picker. * @type number * @default 52 + * @deprecated Please remove this in scope of V9 */ attachmentSelectionBarHeight: number; AttachmentUploadPreviewList: React.ComponentType; - /** - * Custom UI component for [camera selector icon](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/1.png) - * - * **Default: ** - * [CameraSelectorIcon](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/CameraSelectorIcon.tsx) - */ - CameraSelectorIcon: React.ComponentType; - /** - * Custom UI component for the poll creation icon. - * - * **Default: ** - * [CreatePollIcon](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/CreatePollIcon.tsx) - */ - CreatePollIcon: React.ComponentType; - /** - * Custom UI component for [file selector icon](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/1.png) - * - * **Default: ** - * [FileSelectorIcon](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/FileSelectorIcon.tsx) - */ - FileSelectorIcon: React.ComponentType; - /** - * Custom UI component for [image selector icon](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/1.png) - * - * **Default: ** - * [ImageSelectorIcon](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/ImageSelectorIcon.tsx) - */ - ImageSelectorIcon: React.ComponentType; - /** - * Custom UI component for Android's video recorder selector icon. - * - * **Default: ** - * [VideoRecorderSelectorIcon](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/VideoRecorderSelectorIcon.tsx) - */ - VideoRecorderSelectorIcon: React.ComponentType; AudioAttachmentUploadPreview: React.ComponentType; ImageAttachmentUploadPreview: React.ComponentType; FileAttachmentUploadPreview: React.ComponentType; @@ -376,7 +323,6 @@ export type InputMessageInputContextValue = { * - closeAttachmentPicker * - openAttachmentPicker * - openCommandsPicker - * - toggleAttachmentPicker */ InputButtons?: React.ComponentType; openPollCreationDialog?: ({ sendMessage }: Pick) => void; @@ -406,7 +352,7 @@ export const MessageInputProvider = ({ }: PropsWithChildren<{ value: InputMessageInputContextValue; }>) => { - const { closePicker, openPicker, selectedPicker, setSelectedPicker } = + const { closePicker, openPicker, attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext(); const { client } = useChatContext(); const channelCapabilities = useOwnCapabilitiesContext(); @@ -430,7 +376,6 @@ export const MessageInputProvider = ({ const messageComposer = useMessageComposer(); const { attachmentManager, editedMessage } = messageComposer; - const { availableUploadSlots } = useAttachmentManagerState(); /** * These are the RN SDK specific middlewares that are added to the message composer to provide the default behaviour. @@ -462,7 +407,7 @@ export const MessageInputProvider = ({ * Function for capturing a photo and uploading it */ const takeAndUploadImage = useStableCallback(async (mediaType?: MediaTypes) => { - if (!availableUploadSlots) { + if (!attachmentManager.availableUploadSlots) { Alert.alert(t('Maximum number of files reached')); return; } @@ -472,7 +417,7 @@ export const MessageInputProvider = ({ mediaType, }); - if (file.askToOpenSettings) { + if (file.askToOpenSettings && disableAttachmentPicker) { Alert.alert( t('Allow camera access in device settings'), t('Device camera is used to take photos or videos.'), @@ -483,23 +428,27 @@ export const MessageInputProvider = ({ ); } - if (file.cancelled) { - return; + if (file.askToOpenSettings || file.cancelled) { + return file; } await uploadNewFile(file); + + return file; }); /** * Function for picking a photo from native image picker and uploading it */ const pickAndUploadImageFromNativePicker = useStableCallback(async () => { - if (!availableUploadSlots) { + if (!attachmentManager.availableUploadSlots) { Alert.alert(t('Maximum number of files reached')); return; } - const result = await NativeHandlers.pickImage({ maxNumberOfFiles: availableUploadSlots }); + const result = await NativeHandlers.pickImage({ + maxNumberOfFiles: attachmentManager.availableUploadSlots, + }); if (result.askToOpenSettings) { Alert.alert( t('Allow access to your Gallery'), @@ -528,13 +477,13 @@ export const MessageInputProvider = ({ return; } - if (!availableUploadSlots) { + if (!attachmentManager.availableUploadSlots) { Alert.alert(t('Maximum number of files reached')); return; } const result = await NativeHandlers.pickDocument({ - maxNumberOfFiles: availableUploadSlots, + maxNumberOfFiles: attachmentManager.availableUploadSlots, }); if (result.cancelled || !result.assets?.length) { @@ -551,28 +500,27 @@ export const MessageInputProvider = ({ */ const openAttachmentPicker = useCallback(() => { dismissKeyboard(); - setSelectedPicker('images'); - openPicker(); - }, [openPicker, setSelectedPicker]); + const run = () => { + attachmentPickerStore.setSelectedPicker('images'); + openPicker(); + }; + + if (Platform.OS === 'android') { + setTimeout(() => { + run(); + }, 100); + } else { + run(); + } + }, [openPicker, attachmentPickerStore]); /** * Function to close the attachment picker if the MediaLibrary is installed. */ const closeAttachmentPicker = useCallback(() => { - setSelectedPicker(undefined); + attachmentPickerStore.setSelectedPicker(undefined); closePicker(); - }, [closePicker, setSelectedPicker]); - - /** - * Function to toggle the attachment picker if the MediaLibrary is installed. - */ - const toggleAttachmentPicker = useCallback(() => { - if (selectedPicker) { - closeAttachmentPicker(); - } else { - openAttachmentPicker(); - } - }, [closeAttachmentPicker, openAttachmentPicker, selectedPicker]); + }, [closePicker, attachmentPickerStore]); const sendMessage = useStableCallback(async () => { if (inputBoxRef.current) { @@ -683,12 +631,10 @@ export const MessageInputProvider = ({ setInputBoxRef, takeAndUploadImage, thread, - toggleAttachmentPicker, uploadNewFile, ...value, closePollCreationDialog, openPollCreationDialog, - selectedPicker, sendMessage, // overriding the originally passed in sendMessage showPollCreationDialog, audioRecorderManager, diff --git a/package/src/contexts/messageInputContext/__tests__/filePickers.test.tsx b/package/src/contexts/messageInputContext/__tests__/filePickers.test.tsx index 6585f39e1f..4b8ea76ce6 100644 --- a/package/src/contexts/messageInputContext/__tests__/filePickers.test.tsx +++ b/package/src/contexts/messageInputContext/__tests__/filePickers.test.tsx @@ -9,6 +9,7 @@ import { generateFileReference } from '../../../mock-builders/attachments'; import { NativeHandlers } from '../../../native'; +import { AttachmentPickerProvider } from '../../attachmentPickerContext/AttachmentPickerContext'; import { ChannelContextValue, ChannelProvider } from '../../channelContext/ChannelContext'; import { MessageComposerProvider } from '../../messageComposerContext/MessageComposerContext'; import { @@ -31,17 +32,19 @@ const Wrapper = ({ channel, client, props }) => { } as ChannelContextValue } > - - - {props.children} - - + + + + {props.children} + + + ); diff --git a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts index 5596123654..e41127b85d 100644 --- a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts +++ b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts @@ -10,8 +10,6 @@ export const useCreateMessageInputContext = ({ asyncMessagesMultiSendEnabled, asyncMessagesSlideToCancelDistance, AttachButton, - AttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight, attachmentPickerBottomSheetHeight, AttachmentPickerSelectionBar, attachmentSelectionBarHeight, @@ -26,16 +24,13 @@ export const useCreateMessageInputContext = ({ AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem, AutoCompleteSuggestionList, - CameraSelectorIcon, closeAttachmentPicker, closePollCreationDialog, compressImageQuality, CooldownTimer, CreatePollContent, - CreatePollIcon, editMessage, FileAttachmentUploadPreview, - FileSelectorIcon, FileUploadInProgressIndicator, FileUploadRetryIndicator, FileUploadNotSupportedIndicator, @@ -48,7 +43,6 @@ export const useCreateMessageInputContext = ({ hasFilePicker, hasImagePicker, ImageAttachmentUploadPreview, - ImageSelectorIcon, Input, inputBoxRef, InputButtons, @@ -58,7 +52,6 @@ export const useCreateMessageInputContext = ({ openPollCreationDialog, pickAndUploadImageFromNativePicker, pickFile, - selectedPicker, SendButton, sendMessage, SendMessageDisallowedIndicator, @@ -70,10 +63,8 @@ export const useCreateMessageInputContext = ({ StopMessageStreamingButton, takeAndUploadImage, thread, - toggleAttachmentPicker, uploadNewFile, VideoAttachmentUploadPreview, - VideoRecorderSelectorIcon, audioRecorderManager, startVoiceRecording, deleteVoiceRecording, @@ -90,8 +81,6 @@ export const useCreateMessageInputContext = ({ asyncMessagesMultiSendEnabled, asyncMessagesSlideToCancelDistance, AttachButton, - AttachmentPickerBottomSheetHandle, - attachmentPickerBottomSheetHandleHeight, attachmentPickerBottomSheetHeight, AttachmentPickerSelectionBar, attachmentSelectionBarHeight, @@ -106,16 +95,13 @@ export const useCreateMessageInputContext = ({ AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem, AutoCompleteSuggestionList, - CameraSelectorIcon, closeAttachmentPicker, closePollCreationDialog, compressImageQuality, CooldownTimer, CreatePollContent, - CreatePollIcon, editMessage, FileAttachmentUploadPreview, - FileSelectorIcon, FileUploadInProgressIndicator, FileUploadRetryIndicator, FileUploadNotSupportedIndicator, @@ -128,7 +114,6 @@ export const useCreateMessageInputContext = ({ hasFilePicker, hasImagePicker, ImageAttachmentUploadPreview, - ImageSelectorIcon, Input, inputBoxRef, InputButtons, @@ -138,7 +123,6 @@ export const useCreateMessageInputContext = ({ openPollCreationDialog, pickAndUploadImageFromNativePicker, pickFile, - selectedPicker, SendButton, sendMessage, SendMessageDisallowedIndicator, @@ -149,10 +133,8 @@ export const useCreateMessageInputContext = ({ StartAudioRecordingButton, StopMessageStreamingButton, takeAndUploadImage, - toggleAttachmentPicker, uploadNewFile, VideoAttachmentUploadPreview, - VideoRecorderSelectorIcon, audioRecorderManager, startVoiceRecording, deleteVoiceRecording, @@ -160,7 +142,7 @@ export const useCreateMessageInputContext = ({ stopVoiceRecording, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [threadId, showPollCreationDialog, selectedPicker], + [threadId, showPollCreationDialog], ); return messageInputContext; diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index 8c6570d22c..9c17ecaea1 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -122,7 +122,7 @@ export type MessagesContextValue = Pick; /** diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index ea49e2ae35..3c5c88d82e 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -100,9 +100,11 @@ export type Theme = { attachmentPicker: { bottomSheetContentContainer: ViewStyle; durationText: TextStyle; - errorButtonText: TextStyle; - errorContainer: ViewStyle; - errorText: TextStyle; + content: { + container: ViewStyle; + infoContainer: ViewStyle; + text: TextStyle; + }; image: ViewStyle; imageOverlay: ViewStyle; imageOverlaySelectedComponent: { @@ -917,9 +919,11 @@ export const defaultTheme: Theme = { attachmentPicker: { bottomSheetContentContainer: {}, durationText: {}, - errorButtonText: {}, - errorContainer: {}, - errorText: {}, + content: { + container: {}, + infoContainer: {}, + text: {}, + }, handle: { container: {}, indicator: {}, diff --git a/package/src/hooks/index.ts b/package/src/hooks/index.ts index c3d032d78c..cc90b8ba17 100644 --- a/package/src/hooks/index.ts +++ b/package/src/hooks/index.ts @@ -11,3 +11,4 @@ export * from './useClientNotifications'; export * from './useInAppNotificationsState'; export * from './useRAFCoalescedValue'; export * from './useAudioPlayerControl'; +export * from './useAttachmentPickerState'; diff --git a/package/src/hooks/useAttachmentPickerState.ts b/package/src/hooks/useAttachmentPickerState.ts new file mode 100644 index 0000000000..1dee9f01c3 --- /dev/null +++ b/package/src/hooks/useAttachmentPickerState.ts @@ -0,0 +1,13 @@ +import { useStateStore } from './useStateStore'; + +import { useAttachmentPickerContext } from '../contexts'; +import { AttachmentPickerState } from '../state-store/attachment-picker-store'; + +const selector = (nextState: AttachmentPickerState) => ({ + selectedPicker: nextState.selectedPicker, +}); + +export const useAttachmentPickerState = () => { + const { attachmentPickerStore } = useAttachmentPickerContext(); + return useStateStore(attachmentPickerStore.state, selector); +}; diff --git a/package/src/i18n/en.json b/package/src/i18n/en.json index 0610ca467c..de474909e5 100644 --- a/package/src/i18n/en.json +++ b/package/src/i18n/en.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} votes", "{{count}} votes_one": "{{count}} vote", "{{count}} votes_other": "{{count}} votes", - "🏙 Attachment...": "🏙 Attachment..." + "🏙 Attachment...": "🏙 Attachment...", + "You have not granted access to the photo library.": "You have not granted access to the photo library.", + "Change in Settings": "Change in Settings", + "Create a poll and let everyone vote": "Create a poll and let everyone vote", + "Open Camera": "Open Camera", + "Pick a document to share it with everyone": "Pick a document to share it with everyone", + "Pick document": "Pick document", + "Take a video and share": "Take a video and share", + "You have not granted access to your camera": "You have not granted access to your camera" } diff --git a/package/src/i18n/es.json b/package/src/i18n/es.json index 34dc0d1d46..3e7645bb99 100644 --- a/package/src/i18n/es.json +++ b/package/src/i18n/es.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} votos", "{{count}} votes_one": "{{count}} voto", "{{count}} votes_other": "{{count}} votos", - "🏙 Attachment...": "🏙 Adjunto..." + "🏙 Attachment...": "🏙 Adjunto...", + "You have not granted access to the photo library.": "No has concedido acceso a la biblioteca de fotos.", + "Change in Settings": "Cambiar en Ajustes", + "Create a poll and let everyone vote": "Crea una encuesta y deja que todos voten", + "Open Camera": "Abrir camara", + "Pick a document to share it with everyone": "Elige un documento para compartirlo con todos", + "Pick document": "Elegir documento", + "Take a video and share": "Graba un video y compártelo", + "You have not granted access to your camera": "No has concedido acceso a tu camara" } diff --git a/package/src/i18n/fr.json b/package/src/i18n/fr.json index dd99db9648..4227224103 100644 --- a/package/src/i18n/fr.json +++ b/package/src/i18n/fr.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} votes", "{{count}} votes_one": "{{count}} vote", "{{count}} votes_other": "{{count}} votes", - "🏙 Attachment...": "🏙 Pièce jointe..." + "🏙 Attachment...": "🏙 Pièce jointe...", + "You have not granted access to the photo library.": "Vous n'avez pas accordé l'accès à la photothèque.", + "Change in Settings": "Changer dans Réglages", + "Create a poll and let everyone vote": "Créez un sondage et laissez tout le monde voter", + "Open Camera": "Ouvrir la caméra", + "Pick a document to share it with everyone": "Choisissez un document pour le partager avec tout le monde", + "Pick document": "Choisir un document", + "Take a video and share": "Prenez une vidéo et partagez-la", + "You have not granted access to your camera": "Vous n'avez pas accordé l'accès à votre caméra" } diff --git a/package/src/i18n/he.json b/package/src/i18n/he.json index 66dfc4bea6..b7b4b6736b 100644 --- a/package/src/i18n/he.json +++ b/package/src/i18n/he.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} הצבעות", "{{count}} votes_one": "{{count}} הצבעה", "{{count}} votes_other": "{{count}} הצבעות", - "🏙 Attachment...": "🏙 קובץ מצורף..." + "🏙 Attachment...": "🏙 קובץ מצורף...", + "You have not granted access to the photo library.": "לא הענקת גישה לספריית התמונות.", + "Change in Settings": "שנה בהגדרות", + "Create a poll and let everyone vote": "צור סקר ותן לכולם להצביע", + "Open Camera": "פתח מצלמה", + "Pick a document to share it with everyone": "בחר מסמך כדי לשתף אותו עם כולם", + "Pick document": "בחר מסמך", + "Take a video and share": "צלם וידאו ושתף", + "You have not granted access to your camera": "לא הענקת גישה למצלמה שלך" } diff --git a/package/src/i18n/hi.json b/package/src/i18n/hi.json index 417fb29b82..37616e739b 100644 --- a/package/src/i18n/hi.json +++ b/package/src/i18n/hi.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} वोट", "{{count}} votes_one": "{{count}} वोट", "{{count}} votes_other": "{{count}} वोट", - "🏙 Attachment...": "🏙 अटैचमेंट..." + "🏙 Attachment...": "🏙 अटैचमेंट...", + "You have not granted access to the photo library.": "आपने फोटो लाइब्रेरी के लिए अनुमति नहीं दी है।", + "Change in Settings": "सेटिंग्स में बदलें", + "Create a poll and let everyone vote": "पोल बनाएँ और सभी को वोट करने दें", + "Open Camera": "कैमरा खोलें", + "Pick a document to share it with everyone": "सभी के साथ साझा करने के लिए एक दस्तावेज़ चुनें", + "Pick document": "दस्तावेज़ चुनें", + "Take a video and share": "वीडियो लें और साझा करें", + "You have not granted access to your camera": "आपने अपने कैमरे के लिए अनुमति नहीं दी है" } diff --git a/package/src/i18n/it.json b/package/src/i18n/it.json index aec7e7a420..63b86dde74 100644 --- a/package/src/i18n/it.json +++ b/package/src/i18n/it.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} voti", "{{count}} votes_one": "{{count}} voto", "{{count}} votes_other": "{{count}} voti", - "🏙 Attachment...": "🏙 Allegato..." + "🏙 Attachment...": "🏙 Allegato...", + "You have not granted access to the photo library.": "Non hai concesso l'accesso alla libreria foto.", + "Change in Settings": "Cambia in Impostazioni", + "Create a poll and let everyone vote": "Crea un sondaggio e lascia che tutti votino", + "Open Camera": "Apri fotocamera", + "Pick a document to share it with everyone": "Scegli un documento da condividere con tutti", + "Pick document": "Scegli documento", + "Take a video and share": "Registra un video e condividilo", + "You have not granted access to your camera": "Non hai concesso l’accesso alla fotocamera" } diff --git a/package/src/i18n/ja.json b/package/src/i18n/ja.json index c26566fa78..b382532753 100644 --- a/package/src/i18n/ja.json +++ b/package/src/i18n/ja.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}}票", "{{count}} votes_one": "{{count}} 票", "{{count}} votes_other": "{{count}} 票", - "🏙 Attachment...": "🏙 アタッチメント..." + "🏙 Attachment...": "🏙 アタッチメント...", + "You have not granted access to the photo library.": "写真ライブラリへのアクセスが許可されていません。", + "Change in Settings": "設定で変更", + "Create a poll and let everyone vote": "投票を作成してみんなに投票してもらう", + "Open Camera": "カメラを開く", + "Pick a document to share it with everyone": "みんなと共有するドキュメントを選択", + "Pick document": "ドキュメントを選択", + "Take a video and share": "動画を撮影して共有", + "You have not granted access to your camera": "カメラへのアクセスが許可されていません" } diff --git a/package/src/i18n/ko.json b/package/src/i18n/ko.json index 4e93d1d3ac..b22c3a5a05 100644 --- a/package/src/i18n/ko.json +++ b/package/src/i18n/ko.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} 표", "{{count}} votes_one": "{{count}} 표", "{{count}} votes_other": "{{count}} 표", - "🏙 Attachment...": "🏙 부착..." + "🏙 Attachment...": "🏙 부착...", + "You have not granted access to the photo library.": "사진 라이브러리에 대한 접근 권한이 없습니다.", + "Change in Settings": "설정에서 변경", + "Create a poll and let everyone vote": "투표를 만들어 모두가 투표하게 하세요", + "Open Camera": "카메라 열기", + "Pick a document to share it with everyone": "모두와 공유할 문서를 선택하세요", + "Pick document": "문서 선택", + "Take a video and share": "동영상을 촬영하고 공유", + "You have not granted access to your camera": "카메라 접근 권한이 없습니다" } diff --git a/package/src/i18n/nl.json b/package/src/i18n/nl.json index 9bb3a46470..c4af00c2b5 100644 --- a/package/src/i18n/nl.json +++ b/package/src/i18n/nl.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} stemmen", "{{count}} votes_one": "{{count}} stem", "{{count}} votes_other": "{{count}} stemmen", - "🏙 Attachment...": "🏙 Bijlage..." + "🏙 Attachment...": "🏙 Bijlage...", + "You have not granted access to the photo library.": "Je hebt geen toegang tot de fotobibliotheek verleend.", + "Change in Settings": "Wijzigen in Instellingen", + "Create a poll and let everyone vote": "Maak een peiling en laat iedereen stemmen", + "Open Camera": "Camera openen", + "Pick a document to share it with everyone": "Kies een document om het met iedereen te delen", + "Pick document": "Document kiezen", + "Take a video and share": "Maak een video en deel deze", + "You have not granted access to your camera": "Je hebt geen toegang tot je camera verleend" } diff --git a/package/src/i18n/pt-br.json b/package/src/i18n/pt-br.json index fd2ac9ae0d..a4c3893ac4 100644 --- a/package/src/i18n/pt-br.json +++ b/package/src/i18n/pt-br.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} votos", "{{count}} votes_one": "{{count}} voto", "{{count}} votes_other": "{{count}} votos", - "🏙 Attachment...": "🏙 Anexo..." + "🏙 Attachment...": "🏙 Anexo...", + "You have not granted access to the photo library.": "Você não concedeu acesso à biblioteca de fotos.", + "Change in Settings": "Alterar nos Ajustes", + "Create a poll and let everyone vote": "Crie uma enquete e deixe todos votarem", + "Open Camera": "Abrir camera", + "Pick a document to share it with everyone": "Escolha um documento para compartilhar com todos", + "Pick document": "Escolher documento", + "Take a video and share": "Grave um video e compartilhe", + "You have not granted access to your camera": "Você não concedeu acesso à sua câmera" } diff --git a/package/src/i18n/ru.json b/package/src/i18n/ru.json index 01fbf4592b..deb2ad147f 100644 --- a/package/src/i18n/ru.json +++ b/package/src/i18n/ru.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} голосов", "{{count}} votes_one": "{{count}} голос", "{{count}} votes_other": "{{count}} голосов", - "🏙 Attachment...": "🏙 Вложение..." + "🏙 Attachment...": "🏙 Вложение...", + "You have not granted access to the photo library.": "Вы не предоставили доступ к фотобиблиотеке.", + "Change in Settings": "Изменить в настройках", + "Create a poll and let everyone vote": "Создайте опрос и дайте всем проголосовать", + "Open Camera": "Открыть камеру", + "Pick a document to share it with everyone": "Выберите документ, чтобы поделиться им со всеми", + "Pick document": "Выбрать документ", + "Take a video and share": "Снимите видео и поделитесь", + "You have not granted access to your camera": "Вы не предоставили доступ к вашей камере" } diff --git a/package/src/i18n/tr.json b/package/src/i18n/tr.json index eb555200ae..e4d865c9c0 100644 --- a/package/src/i18n/tr.json +++ b/package/src/i18n/tr.json @@ -152,5 +152,13 @@ "{{count}} votes_many": "{{count}} oy", "{{count}} votes_one": "{{count}} oy", "{{count}} votes_other": "{{count}} oy", - "🏙 Attachment...": "🏙 Ek..." + "🏙 Attachment...": "🏙 Ek...", + "You have not granted access to the photo library.": "Fotoğraf kitaplığına erişim izni vermediniz.", + "Change in Settings": "Ayarlar'da değiştir", + "Create a poll and let everyone vote": "Bir anket oluşturun ve herkesin oy vermesine izin verin", + "Open Camera": "Kamerayı aç", + "Pick a document to share it with everyone": "Herkesle paylaşmak için bir belge seçin", + "Pick document": "Belge seç", + "Take a video and share": "Video çek ve paylaş", + "You have not granted access to your camera": "Kameranıza erişim izni vermediniz" } diff --git a/package/src/icons/Camera.tsx b/package/src/icons/Camera.tsx index 6322408150..210ebd6953 100644 --- a/package/src/icons/Camera.tsx +++ b/package/src/icons/Camera.tsx @@ -1,12 +1,18 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; export const Camera = (props: IconProps) => ( - - + + - + ); diff --git a/package/src/icons/CommandsIcon.tsx b/package/src/icons/CommandsIcon.tsx new file mode 100644 index 0000000000..3d507072b1 --- /dev/null +++ b/package/src/icons/CommandsIcon.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; + +export const CommandsIcon = (props: IconProps) => ( + + + +); diff --git a/package/src/icons/FilePickerIcon.tsx b/package/src/icons/FilePickerIcon.tsx new file mode 100644 index 0000000000..06055c879d --- /dev/null +++ b/package/src/icons/FilePickerIcon.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; + +export const FilePickerIcon = (props: IconProps) => ( + + + +); diff --git a/package/src/icons/Flag.tsx b/package/src/icons/Flag.tsx index 45f5f3378b..505631add3 100644 --- a/package/src/icons/Flag.tsx +++ b/package/src/icons/Flag.tsx @@ -1,12 +1,17 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; export const Flag = (props: IconProps) => ( - - + - + ); diff --git a/package/src/icons/GiphyIcon.tsx b/package/src/icons/GiphyIcon.tsx index be574f3a77..c28f6875e7 100644 --- a/package/src/icons/GiphyIcon.tsx +++ b/package/src/icons/GiphyIcon.tsx @@ -1,18 +1,23 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; export const GiphyIcon = (props: IconProps) => ( - - + - - - - - - + + + + + + + + ); diff --git a/package/src/icons/Imgur.tsx b/package/src/icons/Imgur.tsx index dc708c9b16..55532374ad 100644 --- a/package/src/icons/Imgur.tsx +++ b/package/src/icons/Imgur.tsx @@ -1,26 +1,18 @@ import React from 'react'; -import { Circle } from 'react-native-svg'; +import Svg, { Path, Rect } from 'react-native-svg'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import { IconProps } from './utils/base'; export const Imgur = (props: IconProps) => ( - - + + - - - + ); diff --git a/package/src/icons/Mute.tsx b/package/src/icons/Mute.tsx index 9aae840baf..f25292bacb 100644 --- a/package/src/icons/Mute.tsx +++ b/package/src/icons/Mute.tsx @@ -1,12 +1,23 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Mask, Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; export const Mute = (props: IconProps) => ( - - + + + + + - + ); diff --git a/package/src/icons/Picture.tsx b/package/src/icons/Picture.tsx index 7f2ad55e1b..a00e23b6e1 100644 --- a/package/src/icons/Picture.tsx +++ b/package/src/icons/Picture.tsx @@ -1,16 +1,22 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Path } from 'react-native-svg'; -export const Picture = (props: IconProps) => ( - - - - -); +import { IconProps } from './utils/base'; + +// TODO V9: This SVG relies on path fill instead of stroke. We need to refactor how +// SVGs are resolved before we remove the hack of applying the pathFill +// directly. +export const Picture = (props: IconProps) => { + const { stroke, ...rest } = props; + return ( + + + + ); +}; diff --git a/package/src/icons/PollThumbnail.tsx b/package/src/icons/PollThumbnail.tsx index 1eec10add2..47765a735c 100644 --- a/package/src/icons/PollThumbnail.tsx +++ b/package/src/icons/PollThumbnail.tsx @@ -1,12 +1,14 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; export const PollThumbnail = (props: IconProps) => ( - - + - + ); diff --git a/package/src/icons/Recorder.tsx b/package/src/icons/Recorder.tsx index 10e57efc60..d051dc2295 100644 --- a/package/src/icons/Recorder.tsx +++ b/package/src/icons/Recorder.tsx @@ -3,9 +3,9 @@ import React from 'react'; import { IconProps, RootPath, RootSvg } from './utils/base'; export const Recorder = ({ ...rest }: IconProps) => ( - + diff --git a/package/src/icons/Sound.tsx b/package/src/icons/Sound.tsx index 330a0af159..22741f62fe 100644 --- a/package/src/icons/Sound.tsx +++ b/package/src/icons/Sound.tsx @@ -1,12 +1,17 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; export const Sound = (props: IconProps) => ( - - + - + ); diff --git a/package/src/icons/UserAdd.tsx b/package/src/icons/UserAdd.tsx index 6c1aa2f268..a8441421af 100644 --- a/package/src/icons/UserAdd.tsx +++ b/package/src/icons/UserAdd.tsx @@ -1,13 +1,17 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; export const UserAdd = (props: IconProps) => ( - - - + - + ); diff --git a/package/src/icons/UserDelete.tsx b/package/src/icons/UserDelete.tsx index d7f97cff03..192f0f56d6 100644 --- a/package/src/icons/UserDelete.tsx +++ b/package/src/icons/UserDelete.tsx @@ -1,13 +1,17 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; export const UserDelete = (props: IconProps) => ( - - - + - + ); diff --git a/package/src/icons/index.ts b/package/src/icons/index.ts index 9e17a0568a..d48ca39796 100644 --- a/package/src/icons/index.ts +++ b/package/src/icons/index.ts @@ -102,3 +102,5 @@ export * from './DragHandle'; export * from './Back'; export * from './SendPoll'; export * from './UnreadIndicator'; +export * from './FilePickerIcon'; +export * from './CommandsIcon'; diff --git a/package/src/state-store/attachment-picker-store.ts b/package/src/state-store/attachment-picker-store.ts new file mode 100644 index 0000000000..7c88feda4d --- /dev/null +++ b/package/src/state-store/attachment-picker-store.ts @@ -0,0 +1,33 @@ +import { StateStore } from 'stream-chat'; + +export type SelectedPickerType = + | 'images' + | 'files' + | 'camera-photo' + | 'camera-video' + | 'polls' + | 'commands' + | string + | undefined; + +export type AttachmentPickerState = { + selectedPicker: SelectedPickerType; +}; + +const INITIAL_STATE: AttachmentPickerState = { + selectedPicker: undefined, +}; + +export class AttachmentPickerStore { + public state = new StateStore(INITIAL_STATE); + private lastChanged: number = -1; + + setSelectedPicker(value: SelectedPickerType, debounceClose?: boolean) { + const now = Date.now(); + if (debounceClose && !value && now - this.lastChanged < 500) { + return; + } + this.lastChanged = now; + this.state.partialNext({ selectedPicker: value }); + } +} diff --git a/package/src/utils/utils.ts b/package/src/utils/utils.ts index 5d54f00b65..4f8b8c26e5 100644 --- a/package/src/utils/utils.ts +++ b/package/src/utils/utils.ts @@ -225,7 +225,7 @@ export const getDurationLabelFromDuration = (duration: number) => { const ONE_HOUR_IN_MILLISECONDS = ONE_HOUR_IN_SECONDS * 1000; let durationLabel = '00:00'; const isDurationLongerThanHour = duration / ONE_HOUR_IN_MILLISECONDS >= 1; - const formattedDurationParam = isDurationLongerThanHour ? 'HH:mm:ss' : 'mm:ss'; + const formattedDurationParam = isDurationLongerThanHour ? 'HH:mm:ss' : 'm:ss'; const formattedVideoDuration = dayjs .duration(duration, 'milliseconds') .format(formattedDurationParam); @@ -240,7 +240,7 @@ export const formatMsToMinSec = (ms: number) => { const seconds = totalSeconds % 60; const mm = minutes; // no padding for minutes - const ss = String(seconds).padStart(2, '0'); + const ss = minutes ? String(seconds).padStart(2, '0') : String(seconds); return `${mm}m ${ss}s`.replace(/^0m\s/, ''); };