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,
};