diff --git a/package.json b/package.json index e6548a7c..92cd97b4 100644 --- a/package.json +++ b/package.json @@ -179,6 +179,7 @@ "i18next": "22.4.10", "lodash.debounce": "^4.0.8", "moment": "2.29.4", + "react-hook-form": "^7.49.3", "react-i18next": "12.1.5", "react-native-create-thumbnail": "^1.6.4", "react-native-image-picker": "5.1.0", diff --git a/src/components/ChooseCategoryModal/Components/RenderCategories.tsx b/src/components/ChooseCategoryModal/Components/RenderCategories.tsx new file mode 100644 index 00000000..daf35dbd --- /dev/null +++ b/src/components/ChooseCategoryModal/Components/RenderCategories.tsx @@ -0,0 +1,38 @@ +import { Image, Text, TouchableOpacity } from 'react-native'; +import React from 'react'; +import useImage from '../../../hooks/useImage'; +import { SvgXml } from 'react-native-svg'; +import { categoryIcon } from '../../../svg/svg-xml-list'; +import { useStyle } from '../styles'; + +const RenderCategories = ({ + item, + onSelectCategory, +}: { + item: Amity.Category; + onSelectCategory: (id: string, name: string) => void; +}) => { + const avatarURL = useImage({ fileId: item.avatarFileId }); + const styles = useStyle(); + return ( + onSelectCategory(item.categoryId, item.name)} + style={styles.rowContainer} + > + {item.avatarFileId ? ( + + ) : ( + + )} + + {item.name} + + ); +}; + +export default RenderCategories; diff --git a/src/components/ChooseCategoryModal/index.tsx b/src/components/ChooseCategoryModal/index.tsx index 8d5be002..4027e503 100644 --- a/src/components/ChooseCategoryModal/index.tsx +++ b/src/components/ChooseCategoryModal/index.tsx @@ -5,28 +5,32 @@ import { View, Text, Modal, - Image, FlatList, type NativeSyntheticEvent, type NativeScrollEvent, } from 'react-native'; import { SvgXml } from 'react-native-svg'; -import { categoryIcon, closeIcon } from '../../svg/svg-xml-list'; -import useAuth from '../../hooks/useAuth'; -import { getStyles } from './styles'; +import { closeIcon } from '../../svg/svg-xml-list'; +import { useStyle } from './styles'; import { useTheme } from 'react-native-paper'; import type { MyMD3Theme } from 'src/providers/amity-ui-kit-provider'; +import RenderCategories from './Components/RenderCategories'; interface IModal { visible: boolean; userId?: string; onClose: () => void; onSelect: (categoryId: string, categoryName: string) => void; + categoryId?: string; } -const ChooseCategoryModal = ({ visible, onClose, onSelect }: IModal) => { +const ChooseCategoryModal = ({ + visible, + onClose, + onSelect, + categoryId, +}: IModal) => { const theme = useTheme() as MyMD3Theme; - const styles = getStyles(); - const { apiRegion } = useAuth(); + const styles = useStyle(); const [categories, setCategories] = useState>(); const { data: categoriesList, onNextPage } = categories ?? {}; @@ -40,6 +44,12 @@ const ChooseCategoryModal = ({ visible, onClose, onSelect }: IModal) => { (data: Amity.LiveCollection) => { if (data) { setCategories(data); + if (categoryId) { + const currentCategoryName = + data.data.find((item) => item.categoryId === categoryId) + ?.name ?? ''; + onSelect(categoryId, currentCategoryName); + } } } ); @@ -50,34 +60,13 @@ const ChooseCategoryModal = ({ visible, onClose, onSelect }: IModal) => { }; loadCategories(); - }, []); + }, [categoryId, onSelect]); const onSelectCategory = (categoryId: string, categoryName: string) => { onSelect && onSelect(categoryId, categoryName); unSubFunc && unSubFunc(); onClose && onClose(); }; - const renderCategories = ({ item }: { item: Amity.Category }) => { - return ( - onSelectCategory(item.categoryId, item.name)} - style={styles.rowContainer} - > - {item.avatarFileId ? ( - - ) : ( - - )} - - {item.name} - - ); - }; const handleScroll = (event: NativeSyntheticEvent) => { const scrollPosition = event.nativeEvent.contentOffset.y; @@ -112,7 +101,9 @@ const ChooseCategoryModal = ({ visible, onClose, onSelect }: IModal) => { ( + + )} keyExtractor={(item) => item.categoryId} onScroll={handleScroll} /> diff --git a/src/components/ChooseCategoryModal/styles.ts b/src/components/ChooseCategoryModal/styles.ts index 90168768..c5b049dc 100644 --- a/src/components/ChooseCategoryModal/styles.ts +++ b/src/components/ChooseCategoryModal/styles.ts @@ -2,7 +2,7 @@ import { Platform, StyleSheet } from 'react-native'; import { useTheme } from 'react-native-paper'; import type { MyMD3Theme } from 'src/providers/amity-ui-kit-provider'; -export const getStyles = () => { +export const useStyle = () => { const theme = useTheme() as MyMD3Theme; const styles = StyleSheet.create({ diff --git a/src/components/CustomTab/index.tsx b/src/components/CustomTab/index.tsx index e940c725..840c3804 100644 --- a/src/components/CustomTab/index.tsx +++ b/src/components/CustomTab/index.tsx @@ -8,10 +8,11 @@ import { View, } from 'react-native'; import { getStyles } from './styles'; +import { TabName } from '../../enum/tabNameState'; interface ICustomTab { - onTabChange: (tabIndex: number) => any; - tabName: string[]; + onTabChange: (tabName: TabName) => void; + tabName: TabName[]; } const CustomTab = ({ tabName, onTabChange }: ICustomTab): ReactElement => { const styles = getStyles(); @@ -19,9 +20,15 @@ const CustomTab = ({ tabName, onTabChange }: ICustomTab): ReactElement => { const [indicatorAnim] = useState(new Animated.Value(0)); const [tabOneWidth, setTabOneWidth] = useState(0); const [tabTwoWidth, setTabTwoWidth] = useState(0); - const handleTabPress = (tabIndex: number) => { + const handleTabPress = ({ + name, + tabIndex, + }: { + name: TabName; + tabIndex: number; + }) => { setActiveTab(tabIndex); - onTabChange && onTabChange(tabIndex); + onTabChange && onTabChange(name); Animated.timing(indicatorAnim, { toValue: tabIndex, duration: 100, @@ -53,7 +60,7 @@ const CustomTab = ({ tabName, onTabChange }: ICustomTab): ReactElement => { handleTabPress(1)} + onPress={() => handleTabPress({ name: tabName[0], tabIndex: 1 })} > {tabName[0]} @@ -61,7 +68,7 @@ const CustomTab = ({ tabName, onTabChange }: ICustomTab): ReactElement => { handleTabPress(2)} + onPress={() => handleTabPress({ name: tabName[1], tabIndex: 2 })} > {tabName[1]} diff --git a/src/components/MyCommunity/Components/CommunityList.tsx b/src/components/MyCommunity/Components/CommunityList.tsx new file mode 100644 index 00000000..5e321de5 --- /dev/null +++ b/src/components/MyCommunity/Components/CommunityList.tsx @@ -0,0 +1,82 @@ +import { Image, Text, TouchableOpacity, View } from 'react-native'; +import React from 'react'; +import { SvgXml } from 'react-native-svg'; +import { + communityIcon, + officialIcon, + privateIcon, +} from '../../../svg/svg-xml-list'; +import { useStyle } from '../styles'; +import useImage from '../../../hooks/useImage'; +import type { MyMD3Theme } from '../../../providers/amity-ui-kit-provider'; +import { useTheme } from 'react-native-paper'; +import { PrivacyState } from '../../../enum/privacyState'; + +interface ICommunityItems { + communityId: string; + avatarFileId: string; + displayName: string; + isPublic: boolean; + isOfficial: boolean; +} + +const CommunityList = ({ + item, + onClickItem, +}: { + item: ICommunityItems; + onClickItem: (id: string, name: string) => void; +}) => { + const MAX_LENGTH = 6; + const theme = useTheme() as MyMD3Theme; + const styles = useStyle(); + const avatarUrl = useImage({ fileId: item.avatarFileId }); + const getDisplayName = ({ text, type }: { text?: string; type: string }) => { + if (text) { + const reduceLetter = type === PrivacyState.private ? 3 : 0; + if (text!.length > MAX_LENGTH - reduceLetter) { + return text!.substring(0, MAX_LENGTH) + '...'; + } + return text; + } + return 'Display name'; + }; + return ( + onClickItem(item.communityId, item.displayName)} + key={item.communityId} + style={styles.itemContainer} + > + {item.avatarFileId ? ( + + ) : ( + + )} + + {!item.isPublic && ( + + )} + + {getDisplayName({ + text: item.displayName, + type: !item.isPublic ? PrivacyState.private : PrivacyState.public, + })} + + {item.isOfficial && ( + + )} + + + ); +}; + +export default CommunityList; diff --git a/src/components/MyCommunity/index.tsx b/src/components/MyCommunity/index.tsx index d2b60fb1..3aad4ada 100644 --- a/src/components/MyCommunity/index.tsx +++ b/src/components/MyCommunity/index.tsx @@ -1,19 +1,14 @@ -import React, { useEffect, useState } from 'react'; -import { View, Text, ScrollView, Image, TouchableOpacity } from 'react-native'; -import { getStyles } from './styles'; +import React, { useCallback, useState } from 'react'; +import { View, Text, ScrollView, TouchableOpacity } from 'react-native'; +import { useStyle } from './styles'; import { CommunityRepository } from '@amityco/ts-sdk-react-native'; -import { - arrowOutlined, - communityIcon, - officialIcon, - privateIcon, -} from '../../svg/svg-xml-list'; +import { arrowOutlined } from '../../svg/svg-xml-list'; import { SvgXml } from 'react-native-svg'; -import { useNavigation } from '@react-navigation/native'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import useAuth from '../../hooks/useAuth'; import type { MyMD3Theme } from '../../providers/amity-ui-kit-provider'; import { useTheme } from 'react-native-paper'; +import CommunityList from './Components/CommunityList'; interface ICommunityItems { communityId: string; @@ -24,14 +19,9 @@ interface ICommunityItems { } export default function MyCommunity() { const theme = useTheme() as MyMD3Theme; - const styles = getStyles(); - const { apiRegion } = useAuth(); - const maxLength = 6; + const styles = useStyle(); const [communityItems, setCommunityItems] = useState([]); const navigation = useNavigation>(); - const avatarFileURL = (fileId: string) => { - return `https://api.${apiRegion}.amity.co/api/v3/files/${fileId}/download?size=medium`; - }; const queryCommunities = () => { const unsubscribe = CommunityRepository.getCommunities( { membership: 'member', limit: 8 }, @@ -52,19 +42,12 @@ export default function MyCommunity() { ); unsubscribe(); }; - const getDisplayName = (text: string, type: string) => { - if (text) { - const reduceLetter = type === 'private' ? 3 : 0; - if (text!.length > maxLength - reduceLetter) { - return text!.substring(0, maxLength) + '...'; - } - return text; - } - return 'Display name'; - }; - useEffect(() => { - queryCommunities(); - }, []); + + useFocusEffect( + useCallback(() => { + queryCommunities(); + }, []) + ); const onClickItem = (communityId: string, displayName: string) => { navigation.navigate('CommunityHome', { @@ -95,47 +78,11 @@ export default function MyCommunity() { contentContainerStyle={styles.scrollView} > {communityItems.map((item) => ( - onClickItem(item.communityId, item.displayName)} + - {item.avatarFileId ? ( - - ) : ( - - )} - - {!item.isPublic && ( - - )} - - {getDisplayName( - item.displayName, - !item.isPublic ? 'private' : 'public' - )} - - {item.isOfficial && ( - - )} - - + item={item} + onClickItem={onClickItem} + /> ))} diff --git a/src/components/MyCommunity/styles.ts b/src/components/MyCommunity/styles.ts index b2e42d32..f2a3053f 100644 --- a/src/components/MyCommunity/styles.ts +++ b/src/components/MyCommunity/styles.ts @@ -1,7 +1,7 @@ import { StyleSheet } from 'react-native'; import { useTheme } from 'react-native-paper'; import type { MyMD3Theme } from 'src/providers/amity-ui-kit-provider'; -export const getStyles = () => { +export const useStyle = () => { const theme = useTheme() as MyMD3Theme; const styles = StyleSheet.create({ diff --git a/src/enum/imageSizeState.ts b/src/enum/imageSizeState.ts new file mode 100644 index 00000000..97061548 --- /dev/null +++ b/src/enum/imageSizeState.ts @@ -0,0 +1,12 @@ +export enum ImageSizeState { + small = 'small', + medium = 'medium', + large = 'large', + full = 'full', +} + +export type ImageSizeSubset = + | ImageSizeState.small + | ImageSizeState.medium + | ImageSizeState.large + | ImageSizeState.full; diff --git a/src/enum/privacyState.ts b/src/enum/privacyState.ts new file mode 100644 index 00000000..7dbfce2b --- /dev/null +++ b/src/enum/privacyState.ts @@ -0,0 +1,4 @@ +export enum PrivacyState { + private = 'private', + public = 'public', +} diff --git a/src/enum/tabNameState.ts b/src/enum/tabNameState.ts new file mode 100644 index 00000000..c15371bf --- /dev/null +++ b/src/enum/tabNameState.ts @@ -0,0 +1,8 @@ +export enum TabName { + NewsFeed = 'NewsFeed', + Explorer = 'Explorer', + Timeline = 'Timeline', + Gallery = 'Gallery', + Communities = 'Communities', + Accounts = 'Accounts', +} diff --git a/src/hooks/useImage.ts b/src/hooks/useImage.ts new file mode 100644 index 00000000..2b830162 --- /dev/null +++ b/src/hooks/useImage.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react'; +import { FileRepository } from '@amityco/ts-sdk-react-native'; +import { ImageSizeState, ImageSizeSubset } from '../enum/imageSizeState'; + +interface UseImageProps { + fileId: string; + imageSize?: ImageSizeSubset; +} + +const useImage = ({ + fileId, + imageSize = ImageSizeState.medium, +}: UseImageProps) => { + const [imageUrl, setImageUrl] = useState(undefined); + + useEffect(() => { + if (fileId == null) { + setImageUrl(undefined); + return; + } + + async function run() { + const file = await FileRepository.getFile(fileId); + const newImageUrl = !file + ? undefined + : await FileRepository.fileUrlWithSize(file.data.fileUrl, imageSize); + setImageUrl(newImageUrl); + } + run(); + }, [fileId, imageSize]); + + return imageUrl; +}; + +export default useImage; diff --git a/src/providers/Social/communities-sdk.ts b/src/providers/Social/communities-sdk.ts index ed0c0963..d5bf387d 100644 --- a/src/providers/Social/communities-sdk.ts +++ b/src/providers/Social/communities-sdk.ts @@ -51,6 +51,32 @@ export function createCommunity( }); return communityObject; } + +export const updateCommunity = ( + communityId: string, + communityParam: ICreateCommunity +): Promise => { + const communityObject = new Promise(async (resolve, reject) => { + try { + const newCommunity = { + description: communityParam.description, + displayName: communityParam.displayName, + isPublic: communityParam.isPublic, + categoryIds: [communityParam.category], + avatarFileId: communityParam?.avatarFileId ?? undefined, + }; + const { data: community } = await CommunityRepository.updateCommunity( + communityId, + newCommunity + ); + resolve(community); + } catch (error) { + reject(error); + } + }); + return communityObject; +}; + export async function checkCommunityPermission( communityId: string, client: Amity.Client, diff --git a/src/redux/slices/globalfeedSlice.ts b/src/redux/slices/globalfeedSlice.ts index d757f4fd..03566fc5 100644 --- a/src/redux/slices/globalfeedSlice.ts +++ b/src/redux/slices/globalfeedSlice.ts @@ -13,7 +13,14 @@ const globalFeedSlice = createSlice({ initialState, reducers: { updateGlobalFeed: (state, action: PayloadAction) => { - state.postList = [...state.postList, ...action.payload]; + const getUniqueArrayById = (arr: IPost[]) => { + const uniqueIds = new Set(state.postList.map((post) => post.postId)); + return arr.filter((post) => !uniqueIds.has(post.postId)); + }; + state.postList = [ + ...state.postList, + ...getUniqueArrayById(action.payload), + ]; }, updateByPostId: ( diff --git a/src/routes/RouteParamList.tsx b/src/routes/RouteParamList.tsx index 4401dd8d..89ce13a5 100644 --- a/src/routes/RouteParamList.tsx +++ b/src/routes/RouteParamList.tsx @@ -36,6 +36,9 @@ export type RootStackParamList = { EditProfile: { userId: string; }; + EditCommunity: { + communityId: string; + }; AllMyCommunity: undefined; CreateCommunity: undefined; PendingPosts: { communityId: string; isModerator: boolean }; diff --git a/src/routes/SocialNavigator.tsx b/src/routes/SocialNavigator.tsx index f026da38..b61a9b2d 100644 --- a/src/routes/SocialNavigator.tsx +++ b/src/routes/SocialNavigator.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/no-unstable-nested-components */ import { NavigationContainer } from '@react-navigation/native'; import * as React from 'react'; import { @@ -26,9 +27,11 @@ import type { MyMD3Theme } from '../providers/amity-ui-kit-provider'; import { useTheme } from 'react-native-paper'; import { Image, TouchableOpacity } from 'react-native'; import { SvgXml } from 'react-native-svg'; -import { searchIcon } from '../svg/svg-xml-list'; -import { getStyles } from '../routes/style'; +import { closeIcon, searchIcon } from '../svg/svg-xml-list'; +import { useStyles } from '../routes/style'; import BackButton from '../components/BackButton'; +import CloseButton from '../components/CloseButton'; +import EditCommunity from '../screens/EditCommunity/EditCommunity'; export default function SocialNavigator() { const Stack = createNativeStackNavigator(); @@ -40,7 +43,7 @@ export default function SocialNavigator() { const onClickSearch = (navigation: NativeStackNavigationProp) => { navigation.navigate('CommunitySearch'); }; - const styles = getStyles(); + const styles = useStyles(); return ( {isConnected && ( @@ -131,10 +134,57 @@ export default function SocialNavigator() { - - - + ; + }) => ({ + headerLeft: () => ( + { + navigation.goBack(); + }} + style={styles.btnWrap} + > + + + ), + })} + /> + + , + }} + /> + ; + }) => ({ + headerLeft: () => , + title: 'Edit Profile', + headerTitleAlign: 'center', + })} + /> { - // eslint-disable-next-line react-hooks/rules-of-hooks - const theme = useTheme() as MyMD3Theme; +export const useStyles = () => { const styles = StyleSheet.create({ btnWrap: { padding: 5, @@ -13,9 +9,6 @@ export const getStyles = () => { width: 16, height: 12, }, - saveText: { - color: theme.colors.primary, - }, }); return styles; }; diff --git a/src/screens/AllMyCommunity/index.tsx b/src/screens/AllMyCommunity/index.tsx index 4b672210..26aebf1d 100644 --- a/src/screens/AllMyCommunity/index.tsx +++ b/src/screens/AllMyCommunity/index.tsx @@ -1,5 +1,11 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { View, Text, @@ -13,20 +19,14 @@ import { import debounce from 'lodash.debounce'; import { getStyles } from './styles'; import { SvgXml } from 'react-native-svg'; -import { - circleCloseIcon, - closeIcon, - plusIcon, - searchIcon, -} from '../../svg/svg-xml-list'; -import { useNavigation } from '@react-navigation/native'; +import { circleCloseIcon, plusIcon, searchIcon } from '../../svg/svg-xml-list'; import { CommunityRepository } from '@amityco/ts-sdk-react-native'; import type { ISearchItem } from '../../components/SearchItem'; import SearchItem from '../../components/SearchItem'; import { useTheme } from 'react-native-paper'; import type { MyMD3Theme } from '../../providers/amity-ui-kit-provider'; -export default function AllMyCommunity() { +export default function AllMyCommunity({ navigation }) { const theme = useTheme() as MyMD3Theme; const styles = getStyles(); LogBox.ignoreAllLogs(true); @@ -34,31 +34,13 @@ export default function AllMyCommunity() { const [searchType] = useState('community'); const [communities, setCommunities] = useState>(); - const navigation = useNavigation(); const [searchList, setSearchList] = useState([]); const scrollViewRef = useRef(null); const { data: communitiesArr = [], onNextPage } = communities ?? {}; - const goBack = () => { - navigation.goBack(); - }; const onClickCreateCommunity = () => { navigation.navigate('CreateCommunity'); }; - navigation.setOptions({ - // eslint-disable-next-line react/no-unstable-nested-components - headerLeft: () => ( - - - - ), - headerRight: () => ( - - - - ), - headerTitle: 'My Community', - }); const handleChange = (text: string) => { setSearchTerm(text); @@ -109,11 +91,24 @@ export default function AllMyCommunity() { return debounce(handleChange, 500); }, []); + const headerRight = useCallback( + () => ( + + + + ), + [] + ); + useEffect(() => { + navigation.setOptions({ + headerRight: () => headerRight(), + headerTitle: 'My Community', + }); return () => { debouncedResults.cancel(); }; - }); + }, []); const clearButton = () => { setSearchTerm(''); diff --git a/src/screens/CommunityHome/index.tsx b/src/screens/CommunityHome/index.tsx index ab580930..0315d0a0 100644 --- a/src/screens/CommunityHome/index.tsx +++ b/src/screens/CommunityHome/index.tsx @@ -7,11 +7,10 @@ import { } from '@amityco/ts-sdk-react-native'; import React, { type MutableRefObject, - useEffect, useRef, useState, - useLayoutEffect, useCallback, + useMemo, } from 'react'; import { View, @@ -34,9 +33,11 @@ import { MyMD3Theme } from '../../providers/amity-ui-kit-provider'; import { IPost } from '../../components/Social/PostList'; import { amityPostsFormatter } from '../../util/postDataFormatter'; import { checkCommunityPermission } from '../../providers/Social/communities-sdk'; -import { useNavigation } from '@react-navigation/native'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import FloatingButton from '../../components/FloatingButton'; +import useImage from '../../hooks/useImage'; +import { TabName } from '../../enum/tabNameState'; export type FeedRefType = { handleLoadMore: () => void; @@ -54,37 +55,36 @@ export default function CommunityHome({ route }: any) { const [isJoin, setIsJoin] = useState(true); const [communityData, setCommunityData] = useState>(); - + const avatarUrl = useImage({ fileId: communityData?.data.avatarFileId }); const feedRef: MutableRefObject = useRef(null); const scrollViewRef = useRef(null); - const [pendingPosts, setPendingPosts] = useState([]); const [isShowPendingArea, setIsShowPendingArea] = useState(false); const [isUserHasPermission, setIsUserHasPermission] = useState(false); const [postSetting, setPostSetting] = useState(''); + const disposers: Amity.Unsubscriber[] = useMemo(() => [], []); + const isSubscribed = useRef(false); + const subscribePostTopic = useCallback( + (targetType: string) => { + if (isSubscribed.current) return; - const disposers: Amity.Unsubscriber[] = []; - let isSubscribed = false; - - const subscribePostTopic = (targetType: string) => { - if (isSubscribed) return; - - if (targetType === 'community') { - disposers.push( - subscribeTopic( - getCommunityTopic(communityData?.data, SubscriptionLevels.POST), - () => { - // use callback to handle errors with event subscription - } - ) - ); - isSubscribed = true; - } - }; - - const getPendingPosts = async () => { + if (targetType === 'community') { + disposers.push( + subscribeTopic( + getCommunityTopic(communityData?.data, SubscriptionLevels.POST), + () => { + // use callback to handle errors with event subscription + } + ) + ); + isSubscribed.current = true; + } + }, + [communityData?.data, disposers] + ); + const getPendingPosts = useCallback(async () => { const unsubscribe = PostRepository.getPosts( { targetId: communityId, @@ -112,13 +112,15 @@ export default function CommunityHome({ route }: any) { ) { setIsUserHasPermission(true); } - }; + }, [apiRegion, client, communityId, disposers, subscribePostTopic]); - useEffect(() => { - if (postSetting === 'ADMIN_REVIEW_POST_REQUIRED') { - setIsShowPendingArea(true); - } - }, [postSetting]); + useFocusEffect( + useCallback(() => { + if (postSetting === 'ADMIN_REVIEW_POST_REQUIRED') { + setIsShowPendingArea(true); + } + }, [postSetting]) + ); const loadCommunity = useCallback(async () => { try { @@ -141,14 +143,16 @@ export default function CommunityHome({ route }: any) { console.error('Failed to load communities:', error); } }, [communityId]); - useLayoutEffect(() => { - getPendingPosts(); - loadCommunity(); - return () => { - disposers.forEach((fn) => fn()); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getPendingPosts, loadCommunity]); + + useFocusEffect( + useCallback(() => { + getPendingPosts(); + loadCommunity(); + return () => { + disposers.forEach((fn) => fn()); + }; + }, [disposers, getPendingPosts, loadCommunity]) + ); const handleScroll = (event: NativeSyntheticEvent) => { const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent; @@ -196,8 +200,8 @@ export default function CommunityHome({ route }: any) { ); }; - const handleTab = (index: number) => { - console.log('index: ', index); + const handleTab = (tabName: TabName) => { + console.log('index: ', tabName); //this func not implmented yet }; const handleClickPendingArea = () => { @@ -238,7 +242,7 @@ export default function CommunityHome({ route }: any) { const onEditProfileTap = () => { navigation.navigate('EditCommunity', { - communityId: communityId, + communityData, }); }; @@ -254,9 +258,9 @@ export default function CommunityHome({ route }: any) { } {isJoin && isShowPendingArea ? pendingPostArea() : } - + diff --git a/src/screens/CommunitySearch/index.tsx b/src/screens/CommunitySearch/index.tsx index 6ecd51b7..d59da55e 100644 --- a/src/screens/CommunitySearch/index.tsx +++ b/src/screens/CommunitySearch/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import React, { useEffect, useState } from 'react'; import { View, @@ -22,13 +21,19 @@ import type { ISearchItem } from '../../components/SearchItem'; import SearchItem from '../../components/SearchItem'; import { useTheme } from 'react-native-paper'; import type { MyMD3Theme } from '../../providers/amity-ui-kit-provider'; +import { TabName } from '../../enum/tabNameState'; + +enum searchTypeEnum { + user = 'user', + community = 'community', +} export default function CommunitySearch() { const theme = useTheme() as MyMD3Theme; LogBox.ignoreAllLogs(true); const styles = getStyles(); const [searchTerm, setSearchTerm] = useState(''); - const [searchType, setSearchType] = useState('community'); + const [searchType, setSearchType] = useState(searchTypeEnum.community); const [communities, setCommunities] = useState>(); const [usersObject, setUsersObject] = @@ -58,7 +63,7 @@ export default function CommunitySearch() { } else if (searchTerm.length > 0 && searchType === 'user') { searchAccounts(searchTerm); } - }, [searchTerm]); + }, [searchTerm, searchType]); const searchCommunities = (text: string) => { const unsubscribe = CommunityRepository.getCommunities( @@ -90,7 +95,7 @@ export default function CommunitySearch() { }; useEffect(() => { - if (communitiesArr.length > 0 && searchType === 'community') { + if (communitiesArr.length > 0 && searchType === searchTypeEnum.community) { const searchItem: ISearchItem[] = communitiesArr.map((item) => { return { targetId: item?.communityId, @@ -105,7 +110,7 @@ export default function CommunitySearch() { }, [communitiesArr, searchType]); useEffect(() => { - if (userArr && userArr.length > 0 && searchType === 'user') { + if (userArr && userArr.length > 0 && searchType === searchTypeEnum.user) { const searchUsers: ISearchItem[] = userArr.map((item) => { return { targetId: item?.userId, @@ -125,14 +130,14 @@ export default function CommunitySearch() { const cancelSearch = () => { navigation.goBack(); }; - const handleTabChange = (index: number) => { - if (index === 1) { - setSearchType('community'); + const handleTabChange = (tabName: TabName) => { + if (tabName === TabName.Communities) { + setSearchType(searchTypeEnum.community); if (searchTerm.length > 0) { searchCommunities(searchTerm); } - } else if (index === 2) { - setSearchType('user'); + } else if (tabName === TabName.Accounts) { + setSearchType(searchTypeEnum.user); if (searchTerm.length > 0) { searchAccounts(searchTerm); } @@ -164,7 +169,7 @@ export default function CommunitySearch() { diff --git a/src/screens/CreateCommunity/index.tsx b/src/screens/CreateCommunity/index.tsx index c59dd207..091c75fe 100644 --- a/src/screens/CreateCommunity/index.tsx +++ b/src/screens/CreateCommunity/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; // import { useTranslation } from 'react-i18next'; import { @@ -37,6 +37,7 @@ import { ActivityIndicator, useTheme } from 'react-native-paper'; import type { MyMD3Theme } from '../../providers/amity-ui-kit-provider'; import * as ImagePicker from 'expo-image-picker'; import { uploadImageFile } from '../../providers/file-provider'; +import { PrivacyState } from '../../enum/privacyState'; export default function CreateCommunity() { const styles = getStyles(); @@ -88,19 +89,19 @@ export default function CreateCommunity() { // setImage(result.assets[0]?.uri); // } // }; - const uploadFile = async () => { + const uploadFile = useCallback(async () => { const file: Amity.File[] = await uploadImageFile(image); if (file) { setImageFileId(file[0].fileId); setUploadingImage(false); } - }; + }, [image]); useEffect(() => { if (image) { setUploadingImage(true); uploadFile(); } - }, [image]); + }, [image, uploadFile]); const pickImage = async () => { let result = await ImagePicker.launchImageLibraryAsync({ @@ -142,17 +143,12 @@ export default function CreateCommunity() { setSelectedUserList(removedUser); }; - useEffect(() => { - if (isCreating && !uploadingImage) { - onCreateCommunity(); - } - }, [uploadingImage]); - - const onCreateCommunity = async () => { + const onCreateCommunity = useCallback(async () => { setIsCreating(true); if (!uploadingImage) { const userIds: string[] = selectedUserList.map((item) => item.userId); - const isPublic: boolean = selectedId === 'private' ? false : true; + const isPublic: boolean = + selectedId === PrivacyState.private ? false : true; const communityParam: ICreateCommunity = { displayName: communityName, description: aboutText, @@ -169,7 +165,16 @@ export default function CreateCommunity() { }); } } - }; + }, [ + aboutText, + categoryId, + communityName, + imageFileId, + navigation, + selectedId, + selectedUserList, + uploadingImage, + ]); return ( setSelectedId('public')} + onPress={() => setSelectedId(PrivacyState.public)} style={styles.listItem} > @@ -270,17 +275,21 @@ export default function CreateCommunity() { setSelectedId(value)} - value={'public'} - selected={selectedId === 'public'} - color={selectedId === 'public' ? theme.colors.primary : '#444'} + value={PrivacyState.public} + selected={selectedId === PrivacyState.public} + color={ + selectedId === PrivacyState.public + ? theme.colors.primary + : '#444' + } size={17} /> setSelectedId('private')} + onPress={() => setSelectedId(PrivacyState.private)} style={styles.listItem} > @@ -295,16 +304,16 @@ export default function CreateCommunity() { setSelectedId(value)} - value={'private'} - selected={selectedId === 'private'} - color={selectedId === 'private' ? '#1054DE' : '#444'} + value={PrivacyState.private} + selected={selectedId === PrivacyState.private} + color={selectedId === PrivacyState.private ? '#1054DE' : '#444'} size={17} /> - {selectedId === 'private' && ( + {selectedId === PrivacyState.private && ( diff --git a/src/screens/CreatePost/index.tsx b/src/screens/CreatePost/index.tsx index a88e7bae..9d92096d 100644 --- a/src/screens/CreatePost/index.tsx +++ b/src/screens/CreatePost/index.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { TouchableOpacity, View, @@ -83,33 +83,36 @@ const CreatePost = ({ route }: any) => { const videoRef = React.useRef(null); const { client, apiRegion } = useAuth(); - const getCommunityDetail = () => { + const getCommunityDetail = useCallback(() => { if (targetType === 'community') { CommunityRepository.getCommunity(targetId, setCommunityObject); } - }; + }, [targetId, targetType]); useEffect(() => { getCommunityDetail(); - }, [targetId]); - - const checkMention = (inputString: string) => { - // Check if "@" is at the first letter - const startsWithAt = /^@/.test(inputString); - - // Check if "@" is inside the sentence without any letter before "@" - const insideWithoutLetterBefore = /[^a-zA-Z]@/.test(inputString); - - const atSigns = inputString.match(/@/g); - const atSignsNumber = atSigns ? atSigns.length : 0; - if ( - (startsWithAt || insideWithoutLetterBefore) && - atSignsNumber > mentionNames.length - ) { - setIsShowMention(true); - } else { - setIsShowMention(false); - } - }; + }, [getCommunityDetail]); + + const checkMention = useCallback( + (inputString: string) => { + // Check if "@" is at the first letter + const startsWithAt = /^@/.test(inputString); + + // Check if "@" is inside the sentence without any letter before "@" + const insideWithoutLetterBefore = /[^a-zA-Z]@/.test(inputString); + + const atSigns = inputString.match(/@/g); + const atSignsNumber = atSigns ? atSigns.length : 0; + if ( + (startsWithAt || insideWithoutLetterBefore) && + atSignsNumber > mentionNames.length + ) { + setIsShowMention(true); + } else { + setIsShowMention(false); + } + }, + [mentionNames.length] + ); useEffect(() => { if (isShowMention) { const substringBeforeCursor = inputMessage.substring(0, cursorIndex); @@ -122,11 +125,11 @@ const CreatePost = ({ route }: any) => { setCurrentSearchUserName(searchText); } } - }, [cursorIndex]); + }, [cursorIndex, inputMessage, isShowMention]); useEffect(() => { checkMention(inputMessage); - }, [inputMessage]); + }, [checkMention, inputMessage]); const playVideoFullScreen = async (fileUrl: string) => { if (videoRef) { @@ -140,48 +143,8 @@ const CreatePost = ({ route }: any) => { } }; const goBack = () => { - setTimeout(() => { - navigation.goBack(); - }, 300); + navigation.goBack(); }; - navigation.setOptions({ - // eslint-disable-next-line react/no-unstable-nested-components - header: () => ( - - - - - - - {targetName} - - 0 || - displayImages.length > 0 || - displayVideos.length > 0 - ? false - : true - } - onPress={handleCreatePost} - > - 0 || - displayImages.length > 0 || - displayVideos.length > 0 - ? styles.postText - : [styles.postText, styles.disabled] - } - > - Post - - - - - ), - headerTitle: '', - }); const handleCreatePost = async () => { const mentionUserIds: string[] = mentionNames.map((item) => item.targetId); if (displayImages.length > 0) { @@ -482,7 +445,7 @@ const CreatePost = ({ route }: any) => { setCursorIndex(event.nativeEvent.selection.start); }; - const RenderTextWithMention = () => { + const renderTextWithMention = () => { if (mentionsPosition.length === 0) { return {inputMessage}; } @@ -529,10 +492,43 @@ const CreatePost = ({ route }: any) => { }); setMentionNames(checkMentionNames); setMentionsPosition(checkMentionPosition); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [inputMessage]); return ( + + + + + + + {targetName} + + 0 || + displayImages.length > 0 || + displayVideos.length > 0 + ? false + : true + } + onPress={handleCreatePost} + > + 0 || + displayImages.length > 0 || + displayVideos.length > 0 + ? styles.postText + : [styles.postText, styles.disabled] + } + > + Post + + + + { onSelectionChange={handleSelectionChange} /> {mentionNames.length > 0 && ( - - {/* {renderTextWithMention()} */} - - + {renderTextWithMention()} )} {/* */} diff --git a/src/screens/EditCommunity/EditCommunity.tsx b/src/screens/EditCommunity/EditCommunity.tsx new file mode 100644 index 00000000..ecdfac94 --- /dev/null +++ b/src/screens/EditCommunity/EditCommunity.tsx @@ -0,0 +1,418 @@ +import * as React from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { + View, + Text, + TouchableOpacity, + Image, + TextInput, + ScrollView, + Pressable, + FlatList, +} from 'react-native'; +import { SvgXml } from 'react-native-svg'; +import { + arrowOutlined, + closeIcon, + plusIcon, + privateIcon, + publicIcon, +} from '../../svg/svg-xml-list'; +import { useStyles } from './styles'; +import ChooseCategoryModal from '../../components/ChooseCategoryModal'; +import { RadioButton } from 'react-native-radio-buttons-group'; +import AddMembersModal from '../../components/AddMembersModal'; +import type { UserInterface } from 'src/types/user.interface'; +import useAuth from '../../hooks/useAuth'; +import { useTheme } from 'react-native-paper'; +import type { MyMD3Theme } from '../../providers/amity-ui-kit-provider'; +import * as ImagePicker from 'expo-image-picker'; +import { uploadImageFile } from '../../providers/file-provider'; +import { getAvatarURL } from '../../util/apiUtil'; +import { updateCommunity } from '../../providers/Social/communities-sdk'; +import { PrivacyState } from '../../enum/privacyState'; +import { useForm, Controller } from 'react-hook-form'; + +const EditCommunity = ({ navigation, route }) => { + const styles = useStyles(); + const theme = useTheme() as MyMD3Theme; + const { + communityData: { data }, + }: { communityData: { data: Amity.RawCommunity } } = route.params; + const { apiRegion } = useAuth(); + const { + control, + handleSubmit, + formState: { errors, defaultValues }, + watch, + } = useForm({ + defaultValues: { + community_name: data.displayName, + community_description: data.description, + }, + }); + const [image, setImage] = useState(''); + const [categoryName, setCategoryName] = useState(''); + const [categoryId, setCategoryId] = useState(data.categoryIds[0]); + const [categoryModal, setCategoryModal] = useState(false); + const [addMembersModal, setAddMembersModal] = useState(false); + const [isPublic, setisPublic] = useState(data.isPublic); + const [selectedUserList, setSelectedUserList] = useState([]); + const [loading, setLoading] = useState(false); + const [imageFileId, setImageFileId] = useState(data.avatarFileId); + + const MAX_COMMUNITY_NAME_LENGTH = 30; + const MAX_ABOUT_TEXT_LENGTH = 180; + + const onPressUpdateCommunity = useCallback( + async ({ + community_name, + community_description, + }: { + community_name: string; + community_description: string; + }) => { + const communityDetail = { + isPublic: isPublic, + description: community_description, + displayName: community_name, + category: categoryId, + avatarFileId: imageFileId, + }; + try { + setLoading(true); + await updateCommunity(data.communityId, communityDetail); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + navigation.navigate({ + name: 'CommunityHome', + params: { + communityId: data.communityId, + communityName: communityDetail.displayName, + }, + merge: true, + }); + } + }, + [categoryId, data, imageFileId, isPublic, navigation] + ); + + useEffect(() => { + navigation.setOptions({ + headerRight: () => ( + + Save + + ), + }); + }, [ + handleSubmit, + loading, + navigation, + onPressUpdateCommunity, + styles.saveText, + ]); + + useEffect(() => { + data.avatarFileId && setImage(getAvatarURL(apiRegion, data.avatarFileId)); + }, [apiRegion, data.avatarFileId]); + + const uploadFile = useCallback(async () => { + try { + const file: Amity.File[] = await uploadImageFile(image); + if (file) { + setImageFileId(file[0].fileId); + } + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + }, [image]); + + useEffect(() => { + if (image) { + setLoading(true); + uploadFile(); + } + }, [image, uploadFile]); + + const pickImage = async () => { + let result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: false, + quality: 1, + }); + + if (!result.canceled && result.assets && result.assets.length > 0) { + setImage(result.assets[0]?.uri); + } + }; + + const handleSelectCategory = (categoryId: string, categoryName: string) => { + setCategoryId(categoryId); + setCategoryName(categoryName); + }; + + const handleAddMembers = (users: UserInterface[]) => { + setSelectedUserList(users); + }; + + const displayName = (user: string) => { + const maxLength = 10; + if (user) { + if (user!.length > maxLength) { + return user!.substring(0, maxLength) + '..'; + } + return user!; + } + return 'Display name'; + }; + + const onDeleteUserPressed = (user: UserInterface) => { + const removedUser = selectedUserList.filter((item) => item !== user); + setSelectedUserList(removedUser); + }; + + return ( + + + + {image ? ( + + ) : ( + + )} + + Upload Image + + + + + + + Community name * + + + {watch('community_name') + ? `${ + watch('community_name').length + } / ${MAX_COMMUNITY_NAME_LENGTH}` + : `0/ ${MAX_COMMUNITY_NAME_LENGTH}`} + + + ( + + )} + rules={{ required: 'Community name is required!' }} + /> + {errors.community_name && ( + + {errors.community_name.message?.toString()} + + )} + + + + + About + + {watch('community_description') + ? `${ + watch('community_description').length + } / ${MAX_ABOUT_TEXT_LENGTH}` + : `0/ ${MAX_ABOUT_TEXT_LENGTH}`} + + + + ( + + )} + /> + + + + + Category * + + + setCategoryModal(true)} + style={styles.categoryContainer} + > + + {categoryName.length > 0 ? categoryName : 'Select Category'} + + + + + + setisPublic(true)} + style={styles.listItem} + > + + + + + + Public + + Anyone can join, view, and search the posts in this community. + + + setisPublic(true)} + value={PrivacyState.public} + selected={isPublic} + color={isPublic ? theme.colors.primary : '#444'} + size={17} + /> + + + setisPublic(false)} + style={styles.listItem} + > + + + + + + Private + + Only members invited by the moderators can join, view, and + search the posts in this community. + + + setisPublic(false)} + value={PrivacyState.private} + selected={!isPublic} + color={!isPublic ? '#1054DE' : '#444'} + size={17} + /> + + + {!isPublic && ( + + + + Add members * + + + + {selectedUserList.length > 0 && ( + ( + + + + + + {displayName(item.displayName)} + + onDeleteUserPressed(item)} + > + + + + )} + keyExtractor={(item) => item.userId.toString()} + numColumns={2} + /> + )} + + setAddMembersModal(true)} + style={styles.addIcon} + > + + + + + + + )} + + + setCategoryModal(false)} + visible={categoryModal} + categoryId={categoryId} + /> + setAddMembersModal(false)} + visible={addMembersModal} + initUserList={selectedUserList} + /> + + ); +}; + +export default EditCommunity; diff --git a/src/screens/EditCommunity/styles.ts b/src/screens/EditCommunity/styles.ts new file mode 100644 index 00000000..952feb59 --- /dev/null +++ b/src/screens/EditCommunity/styles.ts @@ -0,0 +1,197 @@ +import { StyleSheet } from 'react-native'; +import { useTheme } from 'react-native-paper'; +import type { MyMD3Theme } from 'src/providers/amity-ui-kit-provider'; + +export const useStyles = () => { + const theme = useTheme() as MyMD3Theme; + const styles = StyleSheet.create({ + container: { + paddingBottom: 320, + backgroundColor: theme.colors.screenBackground, + }, + uploadContainer: { + width: '100%', + height: '35%', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#f5f5f5', + }, + defaultImage: { + width: '100%', + height: '100%', + backgroundColor: '#898e9e', + }, + image: { + width: '100%', + height: '100%', + }, + button: { + position: 'absolute', + backgroundColor: 'transparent', + borderColor: 'white', + borderWidth: 1, + paddingVertical: 10, + paddingHorizontal: 20, + borderRadius: 5, + alignItems: 'center', + justifyContent: 'center', + }, + buttonText: { + color: 'white', + fontWeight: 'bold', + }, + btnWrap: { + padding: 10, + }, + allInputContainer: { + paddingVertical: 24, + backgroundColor: theme.colors.background, + }, + inputContainer: { + paddingHorizontal: 16, + justifyContent: 'center', + marginBottom: 24, + }, + + inputTitle: { + fontSize: 17, + fontWeight: '600', + color: theme.colors.base, + }, + requiredField: { + color: 'red', + }, + inputField: { + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + borderRadius: 5, + marginTop: 5, + paddingVertical: 16, + color: theme.colors.base, + }, + inputLengthMeasure: { + alignSelf: 'flex-end', + marginTop: 5, + color: 'grey', + }, + placeHolderText: { + color: '#A5A9B5', + }, + titleRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + categoryContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + addIcon: { + marginHorizontal: 6, + }, + arrowIcon: { + opacity: 0.75, + }, + listItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingBottom: 24, + paddingHorizontal: 16, + }, + avatar: { + width: 40, + height: 40, + borderRadius: 72, + marginRight: 12, + backgroundColor: '#EBECEF', + alignItems: 'center', + justifyContent: 'center', + }, + itemText: { + fontSize: 15, + fontWeight: '600', + color: theme.colors.base, + }, + dotIcon: { + width: 16, + height: 12, + }, + categoryText: { + fontSize: 13, + color: theme.colors.baseShade1, + marginTop: 4, + }, + optionDescription: { + width: '70%', + }, + radioGroup: { + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + marginBottom: 24, + }, + createButton: { + flexDirection: 'row', + backgroundColor: theme.colors.primary, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: 10, + marginHorizontal: 16, + borderRadius: 4, + marginBottom: 24, + }, + createText: { + fontWeight: '600', + fontSize: 15, + color: '#FFFFFF', + }, + addUsersContainer: { + marginVertical: 6, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + paddingBottom: 24, + }, + userItemWrap: { + flexDirection: 'row', + backgroundColor: '#EBECEF', + borderRadius: 24, + padding: 6, + height: 40, + flex: 1, + alignItems: 'center', + justifyContent: 'space-between', + margin: 6, + maxWidth: '45%', + }, + avatarImageContainer: { + overflow: 'hidden', + borderRadius: 40, + width: 30, + height: 30, + marginRight: 5, + }, + avatarImage: { + width: 30, + height: 30, + }, + avatarRow: { + flexDirection: 'row', + alignItems: 'center', + }, + loading: { + marginLeft: 6, + }, + saveText: { + color: theme.colors.primary, + }, + errorText: { + color: theme.colors.error, + }, + }); + + return styles; +}; diff --git a/src/screens/Feed/index.tsx b/src/screens/Feed/index.tsx index cd5f55db..c5b20c3c 100644 --- a/src/screens/Feed/index.tsx +++ b/src/screens/Feed/index.tsx @@ -1,7 +1,6 @@ import React, { forwardRef, useCallback, - useEffect, useImperativeHandle, useState, } from 'react'; @@ -26,6 +25,7 @@ import { amityPostsFormatter } from '../../util/postDataFormatter'; import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '../../redux/store'; import feedSlice from '../../redux/slices/feedSlice'; +import { useFocusEffect } from '@react-navigation/native'; interface IFeed { targetId: string; @@ -84,20 +84,22 @@ function Feed({ targetId, targetType }: IFeed, ref: React.Ref) { subscribePostTopic(targetType, targetId); } ); - setUnSubPageFunc(() => unsubscribe); + setUnSubPageFunc(() => unsubscribe()); }, [subscribePostTopic, targetId, targetType]); const handleLoadMore = () => { if (hasNextPage) { onNextPage && onNextPage(); } }; - useEffect(() => { - getFeed(); - return () => { - unSubFunc && unSubFunc(); - dispatch(clearFeed()); - }; - }, [clearFeed, dispatch, getFeed, unSubFunc]); + useFocusEffect( + useCallback(() => { + getFeed(); + return () => { + unSubFunc && unSubFunc(); + dispatch(clearFeed()); + }; + }, [clearFeed, dispatch, getFeed, unSubFunc]) + ); const getPostList = useCallback(async () => { if (posts.length > 0) { @@ -106,9 +108,11 @@ function Feed({ targetId, targetType }: IFeed, ref: React.Ref) { } }, [dispatch, posts, updateFeed]); - useEffect(() => { - posts && getPostList(); - }, [posts, getPostList]); + useFocusEffect( + useCallback(() => { + posts && getPostList(); + }, [posts, getPostList]) + ); useImperativeHandle(ref, () => ({ handleLoadMore, diff --git a/src/screens/GlobalFeed/index.tsx b/src/screens/GlobalFeed/index.tsx index cbfd7ccd..ae7ffc00 100644 --- a/src/screens/GlobalFeed/index.tsx +++ b/src/screens/GlobalFeed/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; // import { useTranslation } from 'react-i18next'; @@ -17,6 +17,7 @@ import { amityPostsFormatter } from '../../util/postDataFormatter'; import { useDispatch, useSelector } from 'react-redux'; import globalFeedSlice from '../../redux/slices/globalfeedSlice'; import { RootState } from 'src/redux/store'; +import { useFocusEffect } from '@react-navigation/native'; export default function GlobalFeed() { const { postList } = useSelector((state: RootState) => state.globalFeed); @@ -25,7 +26,7 @@ export default function GlobalFeed() { const dispatch = useDispatch(); // ()=> dispatch(updateGlobalFeed()) const styles = getStyles(); - const { client, isConnected } = useAuth(); + const { isConnected } = useAuth(); const [postData, setPostData] = useState(); const { data: posts = [], nextPage } = postData ?? {}; @@ -44,20 +45,24 @@ export default function GlobalFeed() { getGlobalFeedList(nextPage); } }; - useEffect(() => { - if (isConnected) { - getGlobalFeedList(); - } - }, [client, isConnected]); + useFocusEffect( + useCallback(() => { + if (isConnected) { + getGlobalFeedList(); + } + }, [isConnected]) + ); const getPostList = useCallback(async () => { if (posts.length > 0) { const formattedPostList = await amityPostsFormatter(posts); dispatch(updateGlobalFeed(formattedPostList)); } }, [dispatch, posts, updateGlobalFeed]); - useEffect(() => { - posts && getPostList(); - }, [posts, getPostList]); + useFocusEffect( + useCallback(() => { + posts && getPostList(); + }, [getPostList, posts]) + ); const onDeletePost = async (postId: string) => { const isDeleted = await deletePostById(postId); diff --git a/src/screens/Home/index.tsx b/src/screens/Home/index.tsx index cae96a19..5dbe72da 100644 --- a/src/screens/Home/index.tsx +++ b/src/screens/Home/index.tsx @@ -9,8 +9,6 @@ import { Animated, Modal, Pressable, - type StyleProp, - type ImageStyle, LogBox, } from 'react-native'; import { SvgXml } from 'react-native-svg'; @@ -24,14 +22,14 @@ import CreatePostModal from '../../components/CreatePostModal'; import CustomTab from '../../components/CustomTab'; import { useTheme } from 'react-native-paper'; import type { MyMD3Theme } from '../../providers/amity-ui-kit-provider'; +import { TabName } from '../../enum/tabNameState'; LogBox.ignoreAllLogs(true); export default function Home() { // const { t, i18n } = useTranslation(); const styles = getStyles(); const { client } = useAuth(); const theme = useTheme() as MyMD3Theme; - - const [activeTab, setActiveTab] = useState(1); + const [activeTab, setActiveTab] = useState(TabName.NewsFeed); const [isVisible, setIsVisible] = useState(false); const [createPostModalVisible, setCreatePostModalVisible] = useState(false); @@ -39,7 +37,6 @@ export default function Home() { const openCreatePostModal = () => { setCreatePostModalVisible(true); }; - const closeCreatePostModal = () => { setCreatePostModalVisible(false); closeModal(); @@ -67,29 +64,6 @@ export default function Home() { } }, [isVisible, slideAnimation]); - const renderTabComponent = () => { - let globalFeedStyle: StyleProp | StyleProp[] = - styles.visible; - styles.visible; - let exploreStyle: StyleProp | StyleProp[] = - styles.invisible; - styles.visible; - if (activeTab === 2) { - globalFeedStyle = styles.invisible; - exploreStyle = styles.visible; - } - return ( - - - - - - - - - - ); - }; const modalStyle = { transform: [ { @@ -100,18 +74,22 @@ export default function Home() { }, ], }; - const handleTabChange = (index: number) => { - setActiveTab(index); - }; return ( - {/* {renderTabView()} */} - {renderTabComponent()} - + {activeTab === TabName.NewsFeed ? ( + + + + + ) : ( + + + + )} { // Set the headerRight component to a TouchableOpacity navigation.setOptions({ - headerLeft: () => , headerRight: () => ( { @@ -83,29 +82,8 @@ export default function UserProfile({ route }: any) { /> ), - title: '', }); - }, [navigation]); - useEffect(() => { - navigation.setOptions({ - // Header options... - headerRight: () => ( - { - navigation.navigate('UserProfileSetting', { - userId: userId, - follow: followStatus, - }); - }} - > - - - ), - }); - }, [followStatus]); + }, [followStatus, navigation, styles.dotIcon, userId]); useEffect(() => { const unsubscribeFollow = UserRepository.Relationship.getFollowInfo( userId, @@ -123,7 +101,7 @@ export default function UserProfile({ route }: any) { if (value && !value.loading) { setUser(value.data); } else { - console.log('user profile query error ' + JSON.stringify(user)); + console.log('user profile query error ' + JSON.stringify(value)); } }); unsubscribeFollow(); @@ -149,7 +127,7 @@ export default function UserProfile({ route }: any) { // Return the function to unsubscribe from the event so it gets removed on unmount return unsubscribe; - }, [navigation]); + }, [navigation, userId]); const editProfileButton = () => { return ( { - console.log('index: ', index); + const handleTab = (tabName: TabName) => { + console.log('index: ', tabName); //this func not implmented yet }; const handleScroll = (event: NativeSyntheticEvent) => { const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent; @@ -247,7 +225,10 @@ export default function UserProfile({ route }: any) { )} - + {/* { + return `https://api.${apiRegion}.amity.co/api/v3/files/${fileId}/download?size=medium`; +}; diff --git a/yarn.lock b/yarn.lock index 4b68851a..e3daddf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8800,6 +8800,11 @@ react-freeze@^1.0.0: resolved "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz" integrity sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g== +react-hook-form@^7.49.3: + version "7.49.3" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.49.3.tgz#576a4567f8a774830812f4855e89f5da5830435c" + integrity sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ== + react-i18next@12.1.5: version "12.1.5" resolved "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.5.tgz"