diff --git a/src/assets/svg/AdminIcon.tsx b/src/assets/svg/AdminIcon.tsx new file mode 100644 index 000000000..898bfde2f --- /dev/null +++ b/src/assets/svg/AdminIcon.tsx @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: ice License 1.0 + +import {COLORS} from '@constants/colors'; +import React from 'react'; +import Svg, {Path, SvgProps} from 'react-native-svg'; +import {rem} from 'rn-units'; + +export const AdminIcon = ({color = COLORS.white, ...props}: SvgProps) => ( + + + +); diff --git a/src/assets/svg/SpeakerphoneIcon.tsx b/src/assets/svg/SpeakerphoneIcon.tsx new file mode 100644 index 000000000..e89dddf3e --- /dev/null +++ b/src/assets/svg/SpeakerphoneIcon.tsx @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: ice License 1.0 + +import {COLORS} from '@constants/colors'; +import React from 'react'; +import Svg, {Path, SvgProps} from 'react-native-svg'; +import {rem} from 'rn-units'; + +export const SpeakerphoneIcon = ({ + color = COLORS.white, + ...props +}: SvgProps) => ( + + + +); diff --git a/src/components/Inputs/CommonInput/hooks/useLabelAnimation.ts b/src/components/Inputs/CommonInput/hooks/useLabelAnimation.ts index 5b930675b..42709cce9 100644 --- a/src/components/Inputs/CommonInput/hooks/useLabelAnimation.ts +++ b/src/components/Inputs/CommonInput/hooks/useLabelAnimation.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: ice License 1.0 -import {useEffect} from 'react'; +import {useCallback, useEffect} from 'react'; +import {LayoutChangeEvent} from 'react-native'; import { interpolate, useAnimatedStyle, @@ -11,6 +12,32 @@ import { export const useLabelAnimation = (isFocused: boolean, text?: string) => { const focusAnimation = useSharedValue(text ? 1 : 0); + const bodyHeight = useSharedValue(0); + + const labelHeight = useSharedValue(0); + + const onLayoutBody = useCallback( + ({ + nativeEvent: { + layout: {height}, + }, + }: LayoutChangeEvent) => { + bodyHeight.value = height; + }, + [bodyHeight], + ); + + const onLayoutLabel = useCallback( + ({ + nativeEvent: { + layout: {height}, + }, + }: LayoutChangeEvent) => { + labelHeight.value = height; + }, + [labelHeight], + ); + useEffect(() => { focusAnimation.value = withTiming(isFocused || text ? 1 : 0); }, [focusAnimation, isFocused, text]); @@ -19,10 +46,18 @@ export const useLabelAnimation = (isFocused: boolean, text?: string) => { fontSize: interpolate(focusAnimation.value, [0, 1], [16, 12]), transform: [ { - translateY: interpolate(focusAnimation.value, [0, 1], [0, -14]), + translateY: interpolate( + focusAnimation.value, + [0, 1], + [(bodyHeight.value - labelHeight.value) / 2, -6], + ), }, ], })); - return {animatedStyle}; + return { + animatedStyle, + onLayoutBody, + onLayoutLabel, + }; }; diff --git a/src/components/Inputs/CommonInput/index.tsx b/src/components/Inputs/CommonInput/index.tsx index 3786fea69..428434983 100644 --- a/src/components/Inputs/CommonInput/index.tsx +++ b/src/components/Inputs/CommonInput/index.tsx @@ -6,7 +6,13 @@ import {COLORS} from '@constants/colors'; import {CheckMarkThinIcon} from '@svg/CheckMarkThinIcon'; import {t} from '@translations/i18n'; import {font} from '@utils/styles'; -import React, {ReactNode, useRef, useState} from 'react'; +import React, { + forwardRef, + ReactNode, + useImperativeHandle, + useRef, + useState, +} from 'react'; import { ActivityIndicator, StyleProp, @@ -22,6 +28,10 @@ import { import Animated from 'react-native-reanimated'; import {isAndroid, rem} from 'rn-units'; +export type CommonInputRef = { + focus(): void; +}; + export type CommonInputProps = TextInputProps & { label: string; value: string; @@ -37,127 +47,142 @@ export type CommonInputProps = TextInputProps & { postfix?: ReactNode; }; -export const CommonInput = ({ - label, - value, - errorText, - validated, - loading, - icon, - prefix, - postfix, - onChange, - onBlur, - onFocus, - onChangeText, - containerStyle, - editable = true, - style, - ...textInputProps -}: CommonInputProps) => { - const [isFocused, setIsFocused] = useState(false); - const inputRef = useRef(null); +export const CommonInput = forwardRef( + ( + { + label, + value, + errorText, + validated, + loading, + icon, + prefix, + postfix, + onChange, + onBlur, + onFocus, + onChangeText, + containerStyle, + editable = true, + style, + ...textInputProps + }, + ref, + ) => { + const [isFocused, setIsFocused] = useState(false); - const {animatedStyle} = useLabelAnimation(isFocused, value); + const inputRef = useRef(null); - return ( - (onChange ? onChange() : inputRef?.current?.focus())}> - - {icon} - - - {prefix} - {onChange ? ( - - {value} - - ) : ( - { - setIsFocused(false); - onBlur?.(event); - }} - onFocus={event => { - setIsFocused(true); - onFocus?.(event); - }} - onChangeText={newValue => { - onChangeText?.(newValue); - }} - {...textInputProps} - /> - )} + useImperativeHandle(ref, () => ({ + focus: () => inputRef.current?.focus(), + })); + + const {animatedStyle, onLayoutBody, onLayoutLabel} = useLabelAnimation( + isFocused, + value, + ); + + return ( + (onChange ? onChange() : inputRef?.current?.focus())}> + + {icon ? {icon} : null} + + + {prefix} + {onChange ? ( + + {value} + + ) : ( + { + setIsFocused(false); + onBlur?.(event); + }} + onFocus={event => { + setIsFocused(true); + onFocus?.(event); + }} + onChangeText={newValue => { + onChangeText?.(newValue); + }} + {...textInputProps} + /> + )} + + + {errorText || label} + - - {errorText || label} - + {onChange && ( + + + {t('button.change').toUpperCase()} + + + )} + {loading && } + {(!!errorText || validated) && !loading && ( + + {errorText ? ( + ! + ) : ( + + )} + + )} + {postfix} - {onChange && ( - - - {t('button.change').toUpperCase()} - - - )} - {loading && } - {(!!errorText || validated) && !loading && ( - - {errorText ? ( - ! - ) : ( - - )} - - )} - {postfix} - - - ); -}; + + ); + }, +); const RESULT_ICON_SIZE = rem(20); const styles = StyleSheet.create({ container: { - paddingHorizontal: rem(20), - height: rem(56), - borderWidth: 1, - borderRadius: rem(16), - backgroundColor: COLORS.wildSand, + paddingVertical: rem(12), + paddingHorizontal: rem(16), + minHeight: rem(56), flexDirection: 'row', alignItems: 'center', + borderWidth: 1, + borderRadius: rem(16), borderColor: COLORS.wildSand, + backgroundColor: COLORS.wildSand, }, container_error: { borderColor: COLORS.attention, @@ -165,9 +190,11 @@ const styles = StyleSheet.create({ container_focused: { borderColor: COLORS.congressBlue, }, + iconContainer: { + marginRight: rem(10), + }, body: { flex: 1, - marginLeft: rem(10), justifyContent: 'center', }, inputWrapper: { @@ -183,6 +210,7 @@ const styles = StyleSheet.create({ }, label: { position: 'absolute', + top: 0, left: 0, ...font(16, 20, 'medium', 'secondary'), }, diff --git a/src/components/KeyboardAvoider/index.tsx b/src/components/KeyboardAvoider/index.tsx index c596bfda6..5cfe2fb84 100644 --- a/src/components/KeyboardAvoider/index.tsx +++ b/src/components/KeyboardAvoider/index.tsx @@ -1,11 +1,8 @@ // SPDX-License-Identifier: ice License 1.0 +import {commonStyles} from '@constants/styles'; import React, {ReactNode} from 'react'; -import { - KeyboardAvoidingView, - KeyboardAvoidingViewProps, - StyleSheet, -} from 'react-native'; +import {KeyboardAvoidingView, KeyboardAvoidingViewProps} from 'react-native'; import {isIOS} from 'rn-units'; type Props = { @@ -14,13 +11,9 @@ type Props = { export const KeyboardAvoider = ({children, ...props}: Props) => ( {children} ); - -const styles = StyleSheet.create({ - flex: {flex: 1}, -}); diff --git a/src/constants/fonts.ts b/src/constants/fonts.ts index 31aa678cd..60f166c84 100644 --- a/src/constants/fonts.ts +++ b/src/constants/fonts.ts @@ -1,31 +1,52 @@ // SPDX-License-Identifier: ice License 1.0 -export type FontWight = keyof typeof FONT_WEIGHTS; +type ValueOf = T[keyof T]; export const FONT_WEIGHTS = { hairline: '100', + '100': '100', + thin: '200', + '200': '200', + light: '300', + '300': '300', + regular: '400', + '400': '400', + medium: '500', + '500': '500', + semibold: '600', + '600': '600', + bold: '700', + '700': '700', + heavy: '800', + '800': '800', + black: '900', + '900': '900', } as const; export type FontFamily = 'primary'; -export const FONTS: {[font in FontFamily]: {[width in FontWight]: string}} = { +export const FONTS: { + [font in FontFamily]: { + [weight in ValueOf]: string; + }; +} = { primary: { - hairline: 'Lato-Hairline', // 100 - thin: 'Lato-Thin', // 200 - light: 'Lato-Light', // 300 - regular: 'Lato-Regular', // 400 - medium: 'Lato-Medium', // 500 - semibold: 'Lato-Semibold', // 600 - bold: 'Lato-Bold', // 700 - heavy: 'Lato-Heavy', // 800 - black: 'Lato-Black', // 900 + '100': 'Lato-Hairline', + '200': 'Lato-Thin', + '300': 'Lato-Light', + '400': 'Lato-Regular', + '500': 'Lato-Medium', + '600': 'Lato-Semibold', + '700': 'Lato-Bold', + '800': 'Lato-Heavy', + '900': 'Lato-Black', }, }; diff --git a/src/hooks/useActionSheetUpdateAvatar.ts b/src/hooks/useActionSheetUpdateAvatar.ts index a268c4eb1..4091653ed 100644 --- a/src/hooks/useActionSheetUpdateAvatar.ts +++ b/src/hooks/useActionSheetUpdateAvatar.ts @@ -13,11 +13,16 @@ import {useEffect, useState} from 'react'; export type CroppedImage = {mime: string; path: string}; type Props = { + title?: string; onChange: (image: CroppedImage | null) => void; uri?: string; }; -export const useActionSheetUpdateAvatar = ({onChange, uri}: Props) => { +export const useActionSheetUpdateAvatar = ({ + title = t('settings.profile_photo.edit'), + onChange, + uri, +}: Props) => { const navigation = useNavigation>(); @@ -32,7 +37,7 @@ export const useActionSheetUpdateAvatar = ({onChange, uri}: Props) => { const onEditPress = () => { navigation.navigate('ActionSheet', { - title: t('settings.profile_photo.edit'), + title, buttons: [ { icon: ImageIcon, diff --git a/src/navigation/Main.tsx b/src/navigation/Main.tsx index a12889166..1cf0d9c25 100644 --- a/src/navigation/Main.tsx +++ b/src/navigation/Main.tsx @@ -17,6 +17,9 @@ import { } from '@react-navigation/bottom-tabs'; import {NavigatorScreenParams} from '@react-navigation/native'; import {createNativeStackNavigator} from '@react-navigation/native-stack'; +import {ChannelAdministrators} from '@screens/ChatFlow/ChannelAdministrators'; +import {ChannelTypeSelect} from '@screens/ChatFlow/ChannelTypeSelect'; +import {EditChannel} from '@screens/ChatFlow/EditChannel'; import {BalanceHistory} from '@screens/HomeFlow/BalanceHistory'; import {Home} from '@screens/HomeFlow/Home'; import { @@ -134,6 +137,18 @@ export type MainStackParamList = { ProfilePrivacyEditStep1: undefined; ProfilePrivacyEditStep2: undefined; ProfilePrivacyEditStep3: undefined; + 'Chat/EditChannel': { + /** + * null for new channel (create channel flow) + */ + channelId: string | null; + }; + 'Chat/ChannelType': { + channelId: string | null; + }; + 'Chat/ChannelAdministrators': { + channelId: string | null; + }; }; export type HomeTabStackParamList = { @@ -399,6 +414,17 @@ export function MainNavigator() { options={modalOptions} component={JoinTelegramPopUp} /> + + + ); } diff --git a/src/screens/ChatFlow/ChannelAdministrators/components/AddAdministratorButton/index.tsx b/src/screens/ChatFlow/ChannelAdministrators/components/AddAdministratorButton/index.tsx new file mode 100644 index 000000000..3752bb63e --- /dev/null +++ b/src/screens/ChatFlow/ChannelAdministrators/components/AddAdministratorButton/index.tsx @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: ice License 1.0 + +import {Touchable} from '@components/Touchable'; +import {COLORS} from '@constants/colors'; +import {AdminIcon} from '@svg/AdminIcon'; +import {t} from '@translations/i18n'; +import {font} from '@utils/styles'; +import React, {useCallback} from 'react'; +import {StyleSheet, Text, View} from 'react-native'; +import {rem} from 'rn-units'; + +interface Props { + channelId: string | null; +} + +export const AddAdministratorButton = ({}: Props) => { + const onPress = useCallback(() => { + // Add administrator to channelId + }, []); + + return ( + + + + + + + {t('chat.channel_administrators.buttons.add_administrator')} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: rem(16), + flexDirection: 'row', + alignItems: 'center', + }, + iconContainer: { + width: rem(46), + height: rem(46), + justifyContent: 'center', + alignItems: 'center', + borderRadius: rem(12), + backgroundColor: COLORS.aliceBlue, + }, + text: { + marginLeft: rem(12), + ...font(16, 19.2, '900', 'primaryDark'), + flex: 1, + }, +}); diff --git a/src/screens/ChatFlow/ChannelAdministrators/components/UserItem/index.tsx b/src/screens/ChatFlow/ChannelAdministrators/components/UserItem/index.tsx new file mode 100644 index 000000000..b9a793928 --- /dev/null +++ b/src/screens/ChatFlow/ChannelAdministrators/components/UserItem/index.tsx @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: ice License 1.0 + +import {Avatar} from '@components/Avatar/Avatar'; +import {Touchable} from '@components/Touchable'; +import {COLORS} from '@constants/colors'; +import {MainStackParamList} from '@navigation/Main'; +import {useNavigation} from '@react-navigation/native'; +import {NativeStackNavigationProp} from '@react-navigation/native-stack'; +import {userIdSelector} from '@store/modules/Account/selectors'; +import {BinIcon} from '@svg/BinIcon'; +import {t} from '@translations/i18n'; +import {font} from '@utils/styles'; +import React, {memo, useCallback} from 'react'; +import {StyleProp, StyleSheet, Text, View, ViewStyle} from 'react-native'; +import {useSelector} from 'react-redux'; +import {rem} from 'rn-units'; + +interface Props { + style?: StyleProp; + userId: string; +} + +export const UserItem = memo(({style, userId}: Props) => { + const navigation = + useNavigation>(); + + const currentUserId = useSelector(userIdSelector); + + const userAvatarUri = 'https://i.pravatar.cc/300?img=1'; + + const userName = 'User Name'; + + const phoneNumber = '123456789'; + + const onRemove = useCallback(() => { + navigation.navigate('PopUp', { + title: t( + 'chat.channel_administrators.dialogs.delete_administrator.title', + ), + message: t( + 'chat.channel_administrators.dialogs.delete_administrator.message', + ), + buttons: [ + { + text: t('button.cancel'), + preset: 'outlined', + }, + { + text: t( + 'chat.channel_administrators.dialogs.delete_administrator.buttons.delete', + ), + preset: 'destructive', + onPress: () => { + // Remove userId from administrators + }, + }, + ], + }); + }, [navigation]); + + return ( + + + + + + {userName} + + + {phoneNumber ? ( + {phoneNumber} + ) : null} + + + {currentUserId !== userId && ( + + + + )} + + ); +}); + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: rem(16), + flexDirection: 'row', + alignItems: 'center', + }, + + body: { + marginLeft: rem(12), + flex: 1, + justifyContent: 'center', + }, + name: { + ...font(16, null, '700', 'primaryDark'), + }, + phoneNumber: { + paddingTop: rem(3), + ...font(13.5, null, '500', 'emperor'), + }, + + binContainer: { + marginLeft: rem(12), + }, +}); diff --git a/src/screens/ChatFlow/ChannelAdministrators/index.tsx b/src/screens/ChatFlow/ChannelAdministrators/index.tsx new file mode 100644 index 000000000..64a35f00a --- /dev/null +++ b/src/screens/ChatFlow/ChannelAdministrators/index.tsx @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: ice License 1.0 + +import {Touchable} from '@components/Touchable'; +import {COLORS} from '@constants/colors'; +import {commonStyles} from '@constants/styles'; +import BottomSheet, {BottomSheetFlatList} from '@gorhom/bottom-sheet'; +import {useSafeAreaFrame} from '@hooks/useSafeAreaFrame'; +import {useSafeAreaInsets} from '@hooks/useSafeAreaInsets'; +import {MainStackParamList} from '@navigation/Main'; +import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'; +import {userIdSelector} from '@store/modules/Account/selectors'; +import {t} from '@translations/i18n'; +import {font} from '@utils/styles'; +import React, {useCallback, useMemo, useState} from 'react'; +import {LayoutChangeEvent, ListRenderItem} from 'react-native'; +import {StyleSheet, Text, View} from 'react-native'; +import {useSelector} from 'react-redux'; +import {rem} from 'rn-units'; + +import {AddAdministratorButton} from './components/AddAdministratorButton'; +import {UserItem} from './components/UserItem'; + +export const ChannelAdministrators = () => { + const { + params: {channelId}, + } = useRoute>(); + + const navigation = useNavigation(); + + const safeAreaInsets = useSafeAreaInsets(); + + const frame = useSafeAreaFrame(); + + const [contentHeight, setContentHeight] = useState(0); + + const [headerHeight, setHeaderHeight] = useState(0); + + const curentUserId = useSelector(userIdSelector); + + const data = useMemo(() => [curentUserId, 'user1', 'user2'], [curentUserId]); + + const snapPoints = useMemo(() => { + const snapPoint = contentHeight + headerHeight || 1; + + return [Math.min(snapPoint, frame.height + safeAreaInsets.bottom)]; + }, [contentHeight, frame.height, headerHeight, safeAreaInsets.bottom]); + + const onLayoutHeader = useCallback( + ({ + nativeEvent: { + layout: {height}, + }, + }: LayoutChangeEvent) => { + setHeaderHeight(height); + }, + [], + ); + + const onContentSizeChange = useCallback((width: number, height: number) => { + setContentHeight(height); + }, []); + + const renderHeader = useCallback(() => { + return ; + }, [channelId]); + + const renderItem: ListRenderItem = useCallback( + ({item: userId}) => { + return ; + }, + [], + ); + + return ( + + + + + + + + {t('chat.channel_administrators.title')} + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + background: { + flex: 1, + justifyContent: 'flex-end', + backgroundColor: COLORS.transparentBackground, + }, + + titleContainer: { + paddingTop: rem(27), + paddingBottom: rem(20), + paddingHorizontal: rem(16), + }, + titleText: { + ...font(14, 16.8, '600', 'primaryDark'), + textTransform: 'uppercase', + flexGrow: 1, + }, + + item: { + marginTop: rem(16), + }, +}); diff --git a/src/screens/ChatFlow/ChannelTypeSelect/index.tsx b/src/screens/ChatFlow/ChannelTypeSelect/index.tsx new file mode 100644 index 000000000..3b73fe9ca --- /dev/null +++ b/src/screens/ChatFlow/ChannelTypeSelect/index.tsx @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: ice License 1.0 + +import {stopPropagation} from '@components/KeyboardDismiss'; +import {Touchable} from '@components/Touchable'; +import {COLORS} from '@constants/colors'; +import {SCREEN_SIDE_OFFSET} from '@constants/styles'; +import {useSafeAreaInsets} from '@hooks/useSafeAreaInsets'; +import {MainStackParamList} from '@navigation/Main'; +import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'; +import {InfoOutlineIcon} from '@svg/InfoOutlineIcon'; +import {RoundCheckboxActiveIcon} from '@svg/RoundCheckboxActiveIcon'; +import {RoundCheckboxInactiveIcon} from '@svg/RoundCheckboxInactiveIcon'; +import {t} from '@translations/i18n'; +import {font} from '@utils/styles'; +import React, {useCallback, useState} from 'react'; +import {StyleSheet, Text, TouchableWithoutFeedback, View} from 'react-native'; +import Animated, {SlideInDown} from 'react-native-reanimated'; +import {rem} from 'rn-units'; + +type ChannelType = 'public' | 'private'; + +const CHANNEL_TYPES: ChannelType[] = ['public', 'private']; + +export const ChannelTypeSelect = () => { + const { + params: {channelId}, + } = useRoute>(); + + const navigation = useNavigation(); + + const safeAreaInsets = useSafeAreaInsets(); + + const [channelType, setChannelType] = useState('public'); + + const renderChannelType = useCallback( + (type: ChannelType) => { + return ( + { + console.log(`Set '${channelType}' channel type for ${channelId}`); + + setChannelType(type); + + navigation.goBack(); + }}> + {channelType === type ? ( + + ) : ( + + )} + + {t(`chat.channel.type.${type}`)} + + ); + }, + [channelId, channelType, navigation], + ); + + return ( + + + + {t('chat.channel_type.title')} + + {CHANNEL_TYPES.map(renderChannelType)} + + + + + {t('chat.channel_type.info')} + + + + + ); +}; + +const styles = StyleSheet.create({ + background: { + flex: 1, + justifyContent: 'flex-end', + backgroundColor: COLORS.transparentBackground, + }, + container: { + paddingTop: rem(30), + paddingHorizontal: SCREEN_SIDE_OFFSET, + borderTopLeftRadius: rem(20), + borderTopRightRadius: rem(20), + backgroundColor: COLORS.white, + }, + titleText: { + ...font(14, 16.8, '600', 'primaryDark'), + }, + + itemContainer: { + marginTop: rem(24), + flexDirection: 'row', + alignItems: 'center', + }, + itemText: { + marginLeft: rem(12), + ...font(12, 14.4, '400', 'secondary'), + }, + + infoContainer: { + marginTop: rem(24), + padding: rem(16), + flexDirection: 'row', + alignItems: 'center', + borderRadius: rem(16), + backgroundColor: COLORS.aliceBlue, + }, + infoText: { + marginLeft: rem(12), + ...font(12, 14.4, '400', 'secondary'), + }, +}); diff --git a/src/screens/ChatFlow/EditChannel/components/ChannelPhoto/index.tsx b/src/screens/ChatFlow/EditChannel/components/ChannelPhoto/index.tsx new file mode 100644 index 000000000..8883298fc --- /dev/null +++ b/src/screens/ChatFlow/EditChannel/components/ChannelPhoto/index.tsx @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: ice License 1.0 + +import {Touchable} from '@components/Touchable'; +import {COLORS} from '@constants/colors'; +import { + CroppedImage, + useActionSheetUpdateAvatar, +} from '@hooks/useActionSheetUpdateAvatar'; +import {CameraIcon} from '@svg/CameraIcon'; +import {t} from '@translations/i18n'; +import {font} from '@utils/styles'; +import React, {useCallback, useMemo, useState} from 'react'; +import {Image, StyleSheet, Text, View} from 'react-native'; +import {rem} from 'rn-units'; + +export const CHANNEL_PHOTO_SIZE = rem(110); + +export const ChannelPhoto = () => { + const [uri, setUri] = useState(''); + + const onChange = useCallback((image: CroppedImage | null) => { + setUri(image?.path ?? ''); + }, []); + + const {onEditPress, localImage} = useActionSheetUpdateAvatar({ + title: t('chat.edit_channel.add_channel_photo'), + onChange, + uri, + }); + + const imageSource = useMemo(() => { + const currentUri = uri || localImage?.path; + + if (!currentUri) { + return null; + } + + return { + uri: currentUri, + }; + }, [localImage?.path, uri]); + + return ( + + + {imageSource ? ( + + ) : ( + + + + )} + + + + {imageSource + ? t('chat.edit_channel.buttons.change_photo') + : t('chat.edit_channel.buttons.add_photo')} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + justifyContent: 'center', + alignItems: 'center', + }, + + photoContainer: { + width: CHANNEL_PHOTO_SIZE, + height: CHANNEL_PHOTO_SIZE, + borderRadius: rem(20), + borderWidth: rem(4.5), + borderColor: COLORS.white, + backgroundColor: COLORS.white, + overflow: 'hidden', + }, + photo: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: COLORS.secondaryFaint, + }, + + text: { + marginTop: rem(8), + ...font(17, 20.4, '600', 'white'), + }, +}); diff --git a/src/screens/ChatFlow/EditChannel/components/ConfigRow/index.tsx b/src/screens/ChatFlow/EditChannel/components/ConfigRow/index.tsx new file mode 100644 index 000000000..e5de2bd2d --- /dev/null +++ b/src/screens/ChatFlow/EditChannel/components/ConfigRow/index.tsx @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: ice License 1.0 + +import {Touchable} from '@components/Touchable'; +import {COLORS} from '@constants/colors'; +import {ChevronIcon} from '@svg/ChevronIcon'; +import {font} from '@utils/styles'; +import React from 'react'; +import {StyleProp, StyleSheet, Text, View, ViewStyle} from 'react-native'; +import {rem} from 'rn-units'; + +interface Props { + style?: StyleProp; + Icon: React.FC<{width: number; height: number; color: string}>; + title: string; + value: string | number; + onPress(): void; +} + +export const ConfigRow = ({style, Icon, title, value, onPress}: Props) => { + return ( + + + + + + {title} + + {value} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + }, + iconContainer: { + width: rem(44), + height: rem(44), + justifyContent: 'center', + alignItems: 'center', + borderRadius: rem(12), + backgroundColor: COLORS.aliceBlue, + }, + title: { + marginLeft: rem(12), + flex: 1, + ...font(16, 19.2, '900', 'primaryDark'), + }, + value: { + marginLeft: rem(12), + ...font(16, 19.2, '400', 'primaryLight'), + }, + arrow: { + marginLeft: rem(8), + }, +}); diff --git a/src/screens/ChatFlow/EditChannel/index.tsx b/src/screens/ChatFlow/EditChannel/index.tsx new file mode 100644 index 000000000..8bb0ad44a --- /dev/null +++ b/src/screens/ChatFlow/EditChannel/index.tsx @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: ice License 1.0 + +import {PrimaryButton} from '@components/Buttons/PrimaryButton'; +import {CommonInput, CommonInputRef} from '@components/Inputs/CommonInput'; +import {KeyboardAvoider} from '@components/KeyboardAvoider'; +import {LinesBackground} from '@components/LinesBackground'; +import {Touchable} from '@components/Touchable'; +import {COLORS} from '@constants/colors'; +import {commonStyles} from '@constants/styles'; +import {useSafeAreaInsets} from '@hooks/useSafeAreaInsets'; +import {useScrollShadow} from '@hooks/useScrollShadow'; +import {Header} from '@navigation/components/Header'; +import {MainStackParamList} from '@navigation/Main'; +import {RouteProp} from '@react-navigation/native'; +import {NativeStackNavigationProp} from '@react-navigation/native-stack'; +import {AdminIcon} from '@svg/AdminIcon'; +import {BinIcon} from '@svg/BinIcon'; +import {SpeakerphoneIcon} from '@svg/SpeakerphoneIcon'; +import {t} from '@translations/i18n'; +import React, {useCallback, useRef, useState} from 'react'; +import {StyleSheet, View} from 'react-native'; +import Animated from 'react-native-reanimated'; +import {rem} from 'rn-units'; + +import {CHANNEL_PHOTO_SIZE, ChannelPhoto} from './components/ChannelPhoto'; +import {ConfigRow} from './components/ConfigRow'; + +interface Props { + navigation: NativeStackNavigationProp; + route: RouteProp; +} + +export const EditChannel = ({navigation, route}: Props) => { + const {channelId} = route.params; + + const safeAreaInsets = useSafeAreaInsets(); + + const {scrollHandler, shadowStyle} = useScrollShadow(); + + const refDescription = useRef(null); + + const [title, setTitle] = useState(''); + + const [description, setDescription] = useState(''); + + const [channelType, _setChannelType] = useState<'public' | 'private'>( + 'public', + ); + + const [admins, _setAdmins] = useState(['currentUser']); + + const onDeleteChannel = useCallback(() => { + navigation.navigate('PopUp', { + title: t('chat.edit_channel.dialogs.delete_channel.title'), + message: t('chat.edit_channel.dialogs.delete_channel.message'), + buttons: [ + { + text: t('button.cancel'), + preset: 'outlined', + }, + { + text: t('chat.edit_channel.dialogs.delete_channel.buttons.delete'), + preset: 'destructive', + onPress: () => { + // TODO: Close all screens related to this channelId (if not null) + navigation.goBack(); + }, + }, + ], + }); + }, [navigation]); + + const renderDeleteChannelButton = useCallback(() => { + if (!channelId) { + return null; + } + + return ( + + + + ); + }, [channelId, onDeleteChannel]); + + return ( + +
+ + + + + + + + + + refDescription.current?.focus()} + /> + + + + { + navigation.navigate('Chat/ChannelType', { + channelId: null, + }); + }} + /> + + { + navigation.navigate('Chat/ChannelAdministrators', { + channelId: null, + }); + }} + /> + + + + { + navigation.goBack(); + }} + /> + + + + ); +}; + +const CONTAINER_BORDER_RADIUS = rem(30); + +const styles = StyleSheet.create({ + contentContainerStyle: { + paddingTop: rem(16), + flexGrow: 1, + backgroundColor: COLORS.white, + }, + + headerContainer: { + paddingBottom: rem(24) + CONTAINER_BORDER_RADIUS, + alignItems: 'center', + }, + headerBackground: { + ...StyleSheet.absoluteFillObject, + top: CHANNEL_PHOTO_SIZE / 2, + borderTopStartRadius: CONTAINER_BORDER_RADIUS, + borderTopEndRadius: CONTAINER_BORDER_RADIUS, + }, + + contentContainer: { + marginTop: -CONTAINER_BORDER_RADIUS, + paddingTop: rem(24), + paddingHorizontal: rem(16), + flex: 1, + borderTopStartRadius: CONTAINER_BORDER_RADIUS, + borderTopEndRadius: CONTAINER_BORDER_RADIUS, + backgroundColor: COLORS.white, + }, + + item: { + marginTop: rem(24), + }, +}); diff --git a/src/screens/Modals/DateSelector/components/Calendar/index.tsx b/src/screens/Modals/DateSelector/components/Calendar/index.tsx index 5e5cd5d48..2297e216b 100644 --- a/src/screens/Modals/DateSelector/components/Calendar/index.tsx +++ b/src/screens/Modals/DateSelector/components/Calendar/index.tsx @@ -44,16 +44,16 @@ export const Calendar = memo( const theme: Theme = useMemo( () => ({ textDayFontSize: rem(12), - textDayFontWeight: FONT_WEIGHTS.medium, - textDayFontFamily: FONTS.primary.medium, + textDayFontWeight: FONT_WEIGHTS['500'], + textDayFontFamily: FONTS.primary['500'], dayTextColor: COLORS.primaryDark, textMonthFontSize: rem(15), - textMonthFontWeight: FONT_WEIGHTS.regular, - textMonthFontFamily: FONTS.primary.regular, + textMonthFontWeight: FONT_WEIGHTS['400'], + textMonthFontFamily: FONTS.primary['400'], monthTextColor: COLORS.primaryDark, textDayHeaderFontSize: rem(12), - textDayHeaderFontWeight: FONT_WEIGHTS.semibold, - textDayHeaderFontFamily: FONTS.primary.semibold, + textDayHeaderFontWeight: FONT_WEIGHTS['600'], + textDayHeaderFontFamily: FONTS.primary['600'], textSectionTitleColor: COLORS.secondary, }), [], diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index b0c8abc52..f491f9a94 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -893,5 +893,59 @@ "description_part1": "Following us on Instagram is a great way to stay in the loop and connect with other users who are also passionate about ice", "description_part2": "Plus, you'll be the first to know about any new features or promotions that we're launching." } + }, + "chat": { + "channel": { + "type": { + "public": "Public", + "private": "Private" + } + }, + "edit_channel": { + "title_create": "Creating a channel", + "title_edit": "Channel editing", + "add_channel_photo": "Add channel photo", + "labels": { + "administrators": "Administrators", + "channel_type": "Channel type", + "description": "Description", + "invitation_link": "Invitation link", + "title": "Title" + }, + "buttons": { + "add_photo": "Add photo", + "change_photo": "Change photo", + "create_channel": "Create channel", + "save_changes": "Save changes" + }, + "dialogs": { + "delete_channel": { + "title": "Delete channel?", + "message": "All publications and subscribers will be lost. Do you really want to delete the channel?", + "buttons": { + "delete": "Delete" + } + } + } + }, + "channel_type": { + "title": "Choose type of channel", + "info": "Public channels can be found through search and any user can subscribe to them" + }, + "channel_administrators": { + "title": "Channel Management", + "buttons": { + "add_administrator": "Add administrator" + }, + "dialogs": { + "delete_administrator": { + "title": "Delete administrator?", + "message": "Delete this channel administrator?", + "buttons": { + "delete": "Delete" + } + } + } + } } } diff --git a/src/translations/locales/en.json.d.ts b/src/translations/locales/en.json.d.ts index d99060cb2..38013b241 100644 --- a/src/translations/locales/en.json.d.ts +++ b/src/translations/locales/en.json.d.ts @@ -583,4 +583,28 @@ export type Translations = { 'social_media.instagram.title': null; 'social_media.instagram.description_part1': null; 'social_media.instagram.description_part2': null; + 'chat.channel.type.public': null; + 'chat.channel.type.private': null; + 'chat.edit_channel.title_create': null; + 'chat.edit_channel.title_edit': null; + 'chat.edit_channel.add_channel_photo': null; + 'chat.edit_channel.labels.administrators': null; + 'chat.edit_channel.labels.channel_type': null; + 'chat.edit_channel.labels.description': null; + 'chat.edit_channel.labels.invitation_link': null; + 'chat.edit_channel.labels.title': null; + 'chat.edit_channel.buttons.add_photo': null; + 'chat.edit_channel.buttons.change_photo': null; + 'chat.edit_channel.buttons.create_channel': null; + 'chat.edit_channel.buttons.save_changes': null; + 'chat.edit_channel.dialogs.delete_channel.title': null; + 'chat.edit_channel.dialogs.delete_channel.message': null; + 'chat.edit_channel.dialogs.delete_channel.buttons.delete': null; + 'chat.channel_type.title': null; + 'chat.channel_type.info': null; + 'chat.channel_administrators.title': null; + 'chat.channel_administrators.buttons.add_administrator': null; + 'chat.channel_administrators.dialogs.delete_administrator.title': null; + 'chat.channel_administrators.dialogs.delete_administrator.message': null; + 'chat.channel_administrators.dialogs.delete_administrator.buttons.delete': null; }; diff --git a/src/utils/styles.ts b/src/utils/styles.ts index d9a0476e7..ae417c4c3 100644 --- a/src/utils/styles.ts +++ b/src/utils/styles.ts @@ -2,7 +2,7 @@ import {COLORS} from '@constants/colors'; // eslint-disable-next-line no-restricted-imports -import {FontFamily, FONTS, FontWight} from '@constants/fonts'; +import {FONT_WEIGHTS, FontFamily, FONTS} from '@constants/fonts'; import {isRTL} from '@translations/i18n'; import {TextStyle} from 'react-native'; import {rem} from 'rn-units'; @@ -10,7 +10,7 @@ import {rem} from 'rn-units'; export const font = ( fontSize: number, lineHeight?: number | null, - fontWeight: FontWight = 'regular', + fontWeight: keyof typeof FONT_WEIGHTS = 'regular', color: keyof typeof COLORS = 'white', textAlign: TextStyle['textAlign'] = 'left', fontFamily: FontFamily = 'primary', @@ -18,7 +18,7 @@ export const font = ( return { fontSize: rem(fontSize), lineHeight: lineHeight != null ? rem(lineHeight) : undefined, - fontFamily: FONTS[fontFamily][fontWeight], + fontFamily: FONTS[fontFamily][FONT_WEIGHTS[fontWeight]], color: COLORS[color], textAlign, };