diff --git a/package.json b/package.json index 278004e..b413f67 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@gorhom/animated-tabbar": "^2.1.1", + "@gorhom/bottom-sheet": "^3", "@react-native-async-storage/async-storage": "^1.15.1", "@react-native-community/google-signin": "^5.0.0", "@react-native-community/masked-view": "^0.1.10", @@ -48,7 +49,9 @@ "react-native-url-polyfill": "^1.3.0", "react-native-vector-icons": "^8.1.0", "react-native-web": "^0.15.5", - "reactotron-mst": "^3.1.3" + "reactotron-mst": "^3.1.3", + "reanimated-bottom-sheet": "^1.0.0-alpha.22", + "use-memo-one": "^1.1.2" }, "devDependencies": { "@babel/core": "^7.13.14", diff --git a/src/app/App.native.tsx b/src/app/App.native.tsx index ef3492e..6f304af 100644 --- a/src/app/App.native.tsx +++ b/src/app/App.native.tsx @@ -1,12 +1,16 @@ import { NavigationContainer } from "@react-navigation/native"; import React, { useContext } from "react"; -import { Platform } from "react-native"; +import { Platform, StatusBar, View } from "react-native"; import "react-native-gesture-handler"; -import { DarkTheme, Provider as PaperProvider } from "react-native-paper"; +import { + DarkTheme, + Provider as PaperProvider, + Title, + useTheme, +} from "react-native-paper"; import SwipeTabNavigation from "./Routes/MainSwipeNavigation"; import SignInNavigation from "./Routes/SignInNavigation"; import AppContextProvider, { AppContext } from "./utils/appContext"; - // eslint-disable-next-line no-undef const theme: ReactNativePaper.Theme = { ...DarkTheme, @@ -20,7 +24,28 @@ const theme: ReactNativePaper.Theme = { }; const Main = () => { - const { signupDone } = useContext(AppContext); + const { loading, signupDone } = useContext(AppContext); + const { colors, dark } = useTheme(); + if (loading) + return ( + <> + + + Instaclone + + + ); if (signupDone) return ; return ; diff --git a/src/app/Components/Post/PostMenuModal.tsx b/src/app/Components/Post/PostMenuModal.tsx new file mode 100644 index 0000000..5260aa3 --- /dev/null +++ b/src/app/Components/Post/PostMenuModal.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { ToastAndroid, useWindowDimensions, View } from "react-native"; +import { List } from "react-native-paper"; +import { useNavigation } from "@react-navigation/native"; +import PostsStore from "../../store/PostsStore"; + +type ModalProps = { + closeModal: () => void; + ownPost: boolean; + postId: number; + username: string; + viewProfile: () => void; +}; + +export const PostModal: React.FC = ({ + postId, + closeModal, + ownPost, + viewProfile, +}) => { + const { width } = useWindowDimensions(); + const navigation = useNavigation(); + const deletePost = async () => { + try { + await PostsStore.deletePost(postId); + closeModal(); + navigation.goBack(); + } catch (err) { + console.error("[deletePost]", err); + ToastAndroid.show("An error occured", ToastAndroid.LONG); + } + }; + + const openProfile = () => { + closeModal(); + viewProfile(); + }; + + return ( + + {ownPost && ( + + )} + + + + ); +}; diff --git a/src/app/Components/Post/PostShareModal.tsx b/src/app/Components/Post/PostShareModal.tsx new file mode 100644 index 0000000..2d6ce33 --- /dev/null +++ b/src/app/Components/Post/PostShareModal.tsx @@ -0,0 +1,178 @@ +import React, { useContext, useEffect, useState } from "react"; +import { + ActivityIndicator, + FlatList, + StatusBar, + TextInput, + ToastAndroid, + useWindowDimensions, + View, +} from "react-native"; +import { Divider, Text, useTheme } from "react-native-paper"; +import NewChatItem, { + NewChatItemType, +} from "../../Screens/Messages/NewChatItem"; +import { Follower } from "../../store/FollowersStore"; +import MessagesStore, { Message } from "../../store/MessagesStore"; +import { AppContext } from "../../utils/appContext"; +import { newMessageInDb } from "../../utils/supabaseUtils"; +import useChatList, { ChatList } from "../../utils/useChatList"; +import useUser from "../../utils/useUser"; + +type ModalProps = { + closeModal: () => void; + postId: number; +}; + +const PostShareModal: React.FC = ({ postId, closeModal }) => { + const { colors } = useTheme(); + const { width, height } = useWindowDimensions(); + const { messageList, loading } = useChatList(); + const [searchTerm, setSearchTerm] = useState(""); + + const { username: currentUsername } = useContext(AppContext); + const { following } = useUser(currentUsername); + + const [chatList, setChatList] = useState([]); + + const getUsersList = (chatsList: ChatList[], followingList: Follower[]) => { + const followingListData = followingList.map((user) => ({ + username: user.following, + })); + + const chatListData = [...chatsList, ...followingListData]; + return [ + ...new Map( + chatListData.map((item) => [item.username, item]) + ).values(), + ]; + }; + + const [searchResults, setSearchResults] = useState< + NewChatItemType[] | null + >(null); + + useEffect(() => { + if (searchTerm.length > 0) { + setSearchResults( + messageList.filter((item) => + item.username.includes(searchTerm.toLowerCase()) + ) + ); + } else { + setSearchResults(null); + } + }, [messageList, searchTerm]); + + useEffect(() => { + if (messageList && following) + setChatList(getUsersList(messageList, following)); + }, [messageList, following]); + + useEffect(() => { + if (searchTerm.length > 0) { + setSearchResults( + messageList.filter((item) => + item.username.includes(searchTerm.toLowerCase()) + ) + ); + } else { + setSearchResults(null); + } + }, [messageList, searchTerm]); + + const newMessage = async (username: string) => { + const messageToSend = { + imageUrl: undefined, + message_type: "POST", + postId: postId, + receiver: username, + text: undefined, + }; + const newMessageData = await newMessageInDb(messageToSend); + if (newMessageData) { + MessagesStore.addMessage(newMessageData); + ToastAndroid.show("Post sent", ToastAndroid.LONG); + } + closeModal(); + }; + + return ( + + {loading && ( + + + + )} + + + setSearchTerm(text)} + style={{ + flex: 1, + marginHorizontal: 16, + marginVertical: 16, + height: 40, + backgroundColor: "#3a3a3a", + borderRadius: 6, + paddingHorizontal: 16, + color: colors.text, + }} + /> + + + Suggested + + + } + ListEmptyComponent={ + + Nothing to see here, yet. + + } + data={searchResults ? searchResults : chatList} + ItemSeparatorComponent={Divider} + renderItem={({ item }) => ( + + )} + keyExtractor={(item) => item.username} + bouncesZoom + bounces + /> + + ); +}; + +export default PostShareModal; diff --git a/src/app/Components/Post/index.tsx b/src/app/Components/Post/index.tsx index 11faaaa..2abef01 100644 --- a/src/app/Components/Post/index.tsx +++ b/src/app/Components/Post/index.tsx @@ -1,101 +1,32 @@ -import React, { useContext, useEffect, useState } from "react"; -import { - Image, - Text, - ToastAndroid, - useWindowDimensions, - View, -} from "react-native"; +import React, { useState } from "react"; +import { Image, Text, useWindowDimensions } from "react-native"; import { Caption, Card, IconButton, - List, Paragraph, Title, useTheme, } from "react-native-paper"; -import { format, formatDistanceToNow } from "date-fns"; import Icon from "react-native-vector-icons/Ionicons"; -import { AppContext } from "../../utils/appContext"; import { useNavigation } from "@react-navigation/native"; -import Modal from "react-native-modal"; import { UserAvatar } from "../UserAvatar"; -import PostsStore, { Post as PostType } from "../../store/PostsStore"; +import { Post as PostType } from "../../store/PostsStore"; import usePost from "../../utils/usePost"; -import supabaseClient from "../../utils/supabaseClient"; -import { definitions } from "../../types/supabase"; import { getTimeDistance } from "../../utils/utils"; -type ModalProps = { - closeModal: () => void; - ownPost: boolean; - postId: number; - username: string; - viewProfile: () => void; -}; -const PostModal: React.FC = ({ - postId, - closeModal, - ownPost, - viewProfile, -}) => { - const { width } = useWindowDimensions(); - const navigation = useNavigation(); - const deletePost = async () => { - try { - await PostsStore.deletePost(postId); - closeModal(); - navigation.goBack(); - } catch (err) { - console.error("[deletePost]", err); - ToastAndroid.show("An error occured", ToastAndroid.LONG); - } - }; - - const openProfile = () => { - closeModal(); - viewProfile(); - }; - return ( - - {ownPost && ( - - )} - - - - ); -}; - -type User = { - username: string; - profilePic?: string | null; -}; +import { User } from "../../store/UsersStore"; +import useUser from "../../utils/useUser"; type Props = Pick & { - user: User; + user: Pick; + openModal: ( + modalType: "MENU" | "SHARE", + username: string, + postId: number + ) => void; + + closeModal: (modalType: "MENU" | "SHARE") => void; }; const Post: React.FC = ({ @@ -104,6 +35,7 @@ const Post: React.FC = ({ postId, postedAt, user: { username, profilePic }, + openModal, }) => { const navigation = useNavigation(); @@ -113,43 +45,9 @@ const Post: React.FC = ({ const [expandedCaption, setExpandedCaption] = useState(false); const toggleExpandCaption = () => setExpandedCaption(!expandedCaption); - const { username: currentUsername } = useContext(AppContext); - const { liked, likes, toggleLike } = usePost(postId); - const [userProfilePic, setUserProfilePic] = useState(null); - useEffect(() => { - (async () => { - if (!username) return; - if (profilePic) { - setUserProfilePic(profilePic); - return; - } else { - const userRes = await supabaseClient - .from("users") - .select("*") - .eq("username", username); - - if (userRes.error || userRes.data.length === 0) { - console.error( - "[fetchUserProfilePic_Response]", - userRes.error - ); - setUserProfilePic(null); - return; - } else { - if (userRes.data[0].profilePic) - setUserProfilePic(userRes.data[0].profilePic); - } - } - })(); - }, [username, profilePic]); - - const [showModal, setShowModal] = useState(false); - - const openModal = () => setShowModal(true); - - const closeModal = () => setShowModal(false); + const { user } = useUser(username); const goBack = () => navigation.goBack(); @@ -160,29 +58,28 @@ const Post: React.FC = ({ goBack, }); + const openComments = () => + navigation.navigate("Comments", { + post: { + caption, + postedAt, + postId, + }, + user: { + username, + profilePic: profilePic || user?.profilePic, + }, + }); + + const openMenuModal = () => openModal("MENU", username, postId); + const openShareModal = () => openModal("SHARE", username, postId); + return ( <> - - - @@ -190,7 +87,7 @@ const Post: React.FC = ({ title={username} left={() => ( )} @@ -198,7 +95,7 @@ const Post: React.FC = ({ )} titleStyle={{ @@ -245,23 +142,23 @@ const Post: React.FC = ({ paddingRight: 0, }} color={colors.text} - onPress={() => { - navigation.navigate("Comments", { - post: { - caption, - postedAt, - postId, - }, - user: { - username, - profilePic, - }, - }); - }} + onPress={openComments} backgroundColor="transparent" name="chatbubble-outline" size={22} /> + {likes.length > 0 && ( diff --git a/src/app/Components/PostBottomSheetWrapper.tsx b/src/app/Components/PostBottomSheetWrapper.tsx new file mode 100644 index 0000000..31efde8 --- /dev/null +++ b/src/app/Components/PostBottomSheetWrapper.tsx @@ -0,0 +1,163 @@ +import { useNavigation } from "@react-navigation/core"; +import React, { useContext, useEffect, useRef } from "react"; +import { + StyleSheet, + TouchableWithoutFeedback, + useWindowDimensions, + View, +} from "react-native"; +import Animated from "react-native-reanimated"; +import BottomSheet from "reanimated-bottom-sheet"; +import { useMemoOne } from "use-memo-one"; +import { AppContext } from "../utils/appContext"; +import { PostModal } from "./Post/PostMenuModal"; +import PostShareModal from "./Post/PostShareModal"; + +type Props = { + openedModalData: { + modalType: "MENU" | "SHARE"; + username: string; + postId: number; + } | null; + onClose: () => void; +}; +const PostBottomSheetWrapper: React.FC = ({ + openedModalData, + onClose, + children, +}) => { + const navigation = useNavigation(); + const menuModalref = useRef(null); + const shareModalRef = useRef(null); + + const { height } = useWindowDimensions(); + const { username: currentUsername } = useContext(AppContext); + + useEffect(() => { + if (openedModalData?.modalType && openedModalData.postId) { + if (openedModalData.modalType === "MENU") { + menuModalref.current?.snapTo(0); + } else { + shareModalRef.current?.snapTo(0); + } + } + }, [openedModalData]); + + const fall = useMemoOne(() => new Animated.Value(1), []); + + const renderHeader = () => ( + + + + + + ); + + const renderMenuModal = () => + openedModalData && ( + closeModal("MENU")} + /> + ); + + const renderShareModal = () => + openedModalData && ( + closeModal("SHARE")} + /> + ); + + const goBack = () => navigation.goBack(); + + const viewProfile = () => + openedModalData && + navigation.navigate("Profile", { + username: openedModalData?.username, + isCurrentUser: false, + goBack, + }); + + const closeModal = (modalType: "MENU" | "SHARE") => { + if (modalType === "MENU") { + menuModalref.current?.snapTo(2); + } else { + shareModalRef.current?.snapTo(2); + } + }; + + return ( + <> + + + + {children} + + + ); +}; + +const styles = StyleSheet.create({ + panelHeader: { + width: "100%", + height: 24, + alignItems: "center", + borderTopLeftRadius: 15, + borderTopRightRadius: 15, + paddingHorizontal: 16, + }, + panelHandle: { + width: 40, + height: 4, + backgroundColor: "#646464", + borderRadius: 4, + marginVertical: 16, + }, + // Shadow + shadowContainer: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "#000", + }, +}); + +export default PostBottomSheetWrapper; diff --git a/src/app/Components/PostContainer/index.tsx b/src/app/Components/PostContainer/index.tsx deleted file mode 100644 index 2091c38..0000000 --- a/src/app/Components/PostContainer/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import { definitions } from "../../types/supabase"; -import Post from "../Post"; - -export const PostContainer: React.FC<{ - item: definitions["posts"]; -}> = ({ item }) => { - return ( - - ); -}; diff --git a/src/app/Routes/MessageStack.tsx b/src/app/Routes/MessageStack.tsx index 890e6b9..d346494 100644 --- a/src/app/Routes/MessageStack.tsx +++ b/src/app/Routes/MessageStack.tsx @@ -7,11 +7,12 @@ import { } from "../types/navigation"; import Messages from "../Screens/Messages/Messages"; -import MessagesList from "../Screens/Messages"; +import ChatList from "../Screens/Messages/ChatList"; import ProfilePageStack from "./ProfileStack"; import { MaterialTopTabNavigationProp } from "@react-navigation/material-top-tabs"; import PostDetail from "../Screens/Post"; import Comments from "../Screens/Comments"; +import NewChat from "../Screens/Messages/NewChat"; const Stack = createStackNavigator(); @@ -24,13 +25,14 @@ type Props = { }; const MessageStack: React.FC = ({ navigation }) => ( - + + diff --git a/src/app/Screens/Explore/index.tsx b/src/app/Screens/Explore/index.tsx index ad9e78c..6b10b55 100644 --- a/src/app/Screens/Explore/index.tsx +++ b/src/app/Screens/Explore/index.tsx @@ -225,7 +225,7 @@ const Explore: React.FC = ({ navigation }) => { post, user: { username: post.user, - profilePic: null, + profilePic: undefined, }, }); }} diff --git a/src/app/Screens/Home/index.tsx b/src/app/Screens/Home/index.tsx index 4a0dcc8..731375b 100644 --- a/src/app/Screens/Home/index.tsx +++ b/src/app/Screens/Home/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { RefreshControl, FlatList, @@ -13,12 +13,13 @@ import { Text, useTheme, } from "react-native-paper"; -import { PostContainer } from "../../Components/PostContainer"; import { observer } from "mobx-react-lite"; import useFeed from "../../utils/useFeed"; import { HomeStackNavigationParams } from "../../types/navigation"; import { RouteProp } from "@react-navigation/native"; import { StackNavigationProp } from "@react-navigation/stack"; +import Post from "../../Components/Post"; +import PostBottomSheetWrapper from "../../Components/PostBottomSheetWrapper"; type Props = { route: RouteProp; @@ -35,6 +36,29 @@ const Home: React.FC = observer(({ route }) => { const openMessages = () => route.params.rootNavigation?.navigate("Messages"); + const [openedModalData, setOpenedModalData] = useState<{ + modalType: "MENU" | "SHARE"; + username: string; + postId: number; + } | null>(null); + + const closeModal = () => { + setOpenedModalData(null); + }; + const onClose = () => setOpenedModalData(null); + + const openModal = ( + modalType: "MENU" | "SHARE", + username: string, + postId: number + ) => { + setOpenedModalData({ + modalType, + postId: postId, + username: username, + }); + }; + return ( = observer(({ route }) => { barStyle={dark ? "light-content" : "dark-content"} animated /> + @@ -70,54 +97,63 @@ const Home: React.FC = observer(({ route }) => { )} - {!loading && feedPosts && ( - - Nothing to see here, yet. - - } - data={feedPosts - .slice() - .sort( - (a, b) => - new Date(b.postedAt).getTime() - - new Date(a.postedAt).getTime() + + {!loading && feedPosts && ( + + Nothing to see here, yet. + + } + data={feedPosts + .slice() + .sort( + (a, b) => + new Date(b.postedAt).getTime() - + new Date(a.postedAt).getTime() + )} + ItemSeparatorComponent={Divider} + renderItem={({ item }) => ( + )} - ItemSeparatorComponent={Divider} - renderItem={({ item }) => ( - - )} - keyExtractor={(item) => item.postId.toString()} - bouncesZoom - bounces - style={{ - backgroundColor: colors.background, - }} - refreshControl={ - - } - /> - )} + keyExtractor={(item) => item.postId.toString()} + bouncesZoom + bounces + style={{ + backgroundColor: colors.background, + }} + refreshControl={ + + } + /> + )} + ); }); diff --git a/src/app/Screens/Messages/index.tsx b/src/app/Screens/Messages/ChatList.tsx similarity index 75% rename from src/app/Screens/Messages/index.tsx rename to src/app/Screens/Messages/ChatList.tsx index 450d5dd..85e05b7 100644 --- a/src/app/Screens/Messages/index.tsx +++ b/src/app/Screens/Messages/ChatList.tsx @@ -1,6 +1,6 @@ import { RouteProp } from "@react-navigation/core"; import { StackNavigationProp } from "@react-navigation/stack"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { ActivityIndicator, FlatList, @@ -14,23 +14,22 @@ import { TouchableOpacity } from "react-native-gesture-handler"; import { Appbar, Caption, Divider, Text, useTheme } from "react-native-paper"; import { UserAvatar } from "../../Components/UserAvatar"; import { MessageStackNavigationParams } from "../../types/navigation"; -import useMessageList, { MessageList } from "../../utils/useMessageList"; +import useChatList, { ChatList } from "../../utils/useChatList"; +import useUser from "../../utils/useUser"; import { getTimeDistance } from "../../utils/utils"; type Props = { - route: RouteProp; - navigation: StackNavigationProp< - MessageStackNavigationParams, - "MessageList" - >; + route: RouteProp; + navigation: StackNavigationProp; }; type UserItemProps = { - item: MessageList; + item: ChatList; openMessage: (username: string) => void; }; const UserListItem: React.FC = ({ item, openMessage }) => { + const { user } = useUser(item.username); return ( = ({ item, openMessage }) => { }} onPress={() => openMessage(item.username)} > - + = ({ item, openMessage }) => { const MessagesList: React.FC = ({ navigation, route }) => { const { colors, dark } = useTheme(); const { height } = useWindowDimensions(); - const { messageList, loading, fetchMessageList } = useMessageList(); + const { messageList, loading, fetchMessageList } = useChatList(); const [searchTerm, setSearchTerm] = useState(""); - const newMessage = () => { - console.log("new"); - }; + const newMessage = () => navigation.navigate("NewChat"); + const goBack = () => route.params.rootNavigation ? route.params.rootNavigation.goBack() @@ -90,6 +88,20 @@ const MessagesList: React.FC = ({ navigation, route }) => { }); }; + const [searchResults, setSearchResults] = useState(); + + useEffect(() => { + if (searchTerm.length > 0) { + setSearchResults( + messageList.filter((item) => + item.username.includes(searchTerm.toLowerCase()) + ) + ); + } else { + setSearchResults(null); + } + }, [messageList, searchTerm]); + return ( = ({ navigation, route }) => { > - + {loading && ( @@ -159,13 +171,17 @@ const MessagesList: React.FC = ({ navigation, route }) => { Nothing to see here, yet. } - data={messageList - .slice() - .sort( - (a, b) => - new Date(b.lastMessageAt).getTime() - - new Date(a.lastMessageAt).getTime() - )} + data={ + searchResults + ? searchResults + : messageList + .slice() + .sort( + (a, b) => + new Date(b.lastMessageAt).getTime() - + new Date(a.lastMessageAt).getTime() + ) + } ItemSeparatorComponent={Divider} renderItem={({ item }) => ( diff --git a/src/app/Screens/Messages/NewChat.tsx b/src/app/Screens/Messages/NewChat.tsx new file mode 100644 index 0000000..a154e19 --- /dev/null +++ b/src/app/Screens/Messages/NewChat.tsx @@ -0,0 +1,189 @@ +import { RouteProp } from "@react-navigation/core"; +import { StackNavigationProp } from "@react-navigation/stack"; +import React, { useContext, useEffect, useState } from "react"; +import { + ActivityIndicator, + FlatList, + RefreshControl, + StatusBar, + TextInput, + useWindowDimensions, + View, +} from "react-native"; +import { Appbar, Divider, Text, useTheme } from "react-native-paper"; +import { Follower } from "../../store/FollowersStore"; +import { MessageStackNavigationParams } from "../../types/navigation"; +import { AppContext } from "../../utils/appContext"; +import useChatList, { ChatList } from "../../utils/useChatList"; +import useUser from "../../utils/useUser"; +import NewChatItem, { NewChatItemType } from "./NewChatItem"; + +type Props = { + route: RouteProp; + navigation: StackNavigationProp; +}; + +export const NewChat: React.FC = ({ navigation }) => { + const { colors, dark } = useTheme(); + const { height } = useWindowDimensions(); + const { messageList, loading, fetchMessageList } = useChatList(); + const [searchTerm, setSearchTerm] = useState(""); + + const { username: currentUsername } = useContext(AppContext); + const { following } = useUser(currentUsername); + + const [chatList, setChatList] = useState([]); + + const getUsersList = (chatsList: ChatList[], followingList: Follower[]) => { + const followingListData = followingList.map((user) => ({ + username: user.following, + })); + + const chatListData = [...chatsList, ...followingListData]; + return [ + ...new Map( + chatListData.map((item) => [item.username, item]) + ).values(), + ]; + }; + + const [searchResults, setSearchResults] = useState< + NewChatItemType[] | null + >(); + + useEffect(() => { + if (searchTerm.length > 0) { + setSearchResults( + messageList.filter((item) => + item.username.includes(searchTerm.toLowerCase()) + ) + ); + } else { + setSearchResults(null); + } + }, [messageList, searchTerm]); + + useEffect(() => { + if (messageList && following) + setChatList(getUsersList(messageList, following)); + }, [messageList, following]); + + const goBack = () => navigation.goBack(); + + const openMessage = (username: string) => { + navigation.navigate("Messages", { + username, + }); + }; + + useEffect(() => { + if (searchTerm.length > 0) { + setSearchResults( + messageList.filter((item) => + item.username.includes(searchTerm.toLowerCase()) + ) + ); + } else { + setSearchResults(null); + } + }, [messageList, searchTerm]); + + return ( + + + + + + + + {loading && ( + + + + )} + + + setSearchTerm(text)} + style={{ + flex: 1, + marginHorizontal: 16, + marginVertical: 16, + height: 40, + backgroundColor: "#3a3a3a", + borderRadius: 6, + paddingHorizontal: 16, + color: colors.text, + }} + /> + + + Suggested + + + } + ListEmptyComponent={ + + Nothing to see here, yet. + + } + data={searchResults ? searchResults : chatList.slice()} + ItemSeparatorComponent={Divider} + renderItem={({ item }) => ( + + )} + keyExtractor={(item) => item.username} + bouncesZoom + bounces + style={{ + backgroundColor: colors.background, + }} + refreshControl={ + + } + /> + + ); +}; + +export default NewChat; diff --git a/src/app/Screens/Messages/NewChatItem.tsx b/src/app/Screens/Messages/NewChatItem.tsx new file mode 100644 index 0000000..b71dd7b --- /dev/null +++ b/src/app/Screens/Messages/NewChatItem.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { View } from "react-native"; +import { TouchableOpacity } from "react-native-gesture-handler"; +import { Caption, Text } from "react-native-paper"; +import { UserAvatar } from "../../Components/UserAvatar"; +import useUser from "../../utils/useUser"; + +export type NewChatItemType = { + username: string; +}; + +type NewChatItemProps = { + item: NewChatItemType; + openMessage?: (username: string) => void; +}; + +const NewChatItem: React.FC = ({ item, openMessage }) => { + const { user } = useUser(item.username); + return ( + + openMessage + ? openMessage(item.username) + : console.log("no open message") + } + > + + + + {item.username} + + {user?.name} + + + ); +}; + +export default NewChatItem; diff --git a/src/app/Screens/Post/index.tsx b/src/app/Screens/Post/index.tsx index 69fe110..34acdfb 100644 --- a/src/app/Screens/Post/index.tsx +++ b/src/app/Screens/Post/index.tsx @@ -1,9 +1,10 @@ import { RouteProp } from "@react-navigation/native"; import { StackNavigationProp } from "@react-navigation/stack"; -import React from "react"; +import React, { useState } from "react"; import { View } from "react-native"; import { Appbar, useTheme } from "react-native-paper"; import Post from "../../Components/Post"; +import PostBottomSheetWrapper from "../../Components/PostBottomSheetWrapper"; import { ExploreStackNavigationParams } from "../../types/navigation"; type Props = { @@ -13,6 +14,29 @@ type Props = { const PostDetail: React.FC = ({ route, navigation }) => { const { colors } = useTheme(); + const [openedModalData, setOpenedModalData] = useState<{ + modalType: "MENU" | "SHARE"; + username: string; + postId: number; + } | null>(null); + + const closeModal = () => { + setOpenedModalData(null); + }; + const onClose = () => setOpenedModalData(null); + + const openModal = ( + modalType: "MENU" | "SHARE", + username: string, + postId: number + ) => { + setOpenedModalData({ + modalType, + postId: postId, + username: username, + }); + }; + return ( = ({ route, navigation }) => { - + + + ); }; diff --git a/src/app/Screens/PostList/index.tsx b/src/app/Screens/PostList/index.tsx index 3b36109..981c599 100644 --- a/src/app/Screens/PostList/index.tsx +++ b/src/app/Screens/PostList/index.tsx @@ -1,11 +1,12 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Appbar, Divider, useTheme } from "react-native-paper"; import { FlatList, StatusBar, useWindowDimensions } from "react-native"; import { StackNavigationProp } from "@react-navigation/stack"; import { ProfileStackParams } from "../../types/navigation"; import { RouteProp } from "@react-navigation/native"; -import { PostContainer } from "../../Components/PostContainer"; import { definitions } from "../../types/supabase"; +import Post from "../../Components/Post"; +import PostBottomSheetWrapper from "../../Components/PostBottomSheetWrapper"; type Props = { navigation: StackNavigationProp; @@ -54,6 +55,29 @@ const PostList: React.FC = ({ navigation, route }) => { }; }; + const [openedModalData, setOpenedModalData] = useState<{ + modalType: "MENU" | "SHARE"; + username: string; + postId: number; + } | null>(null); + + const closeModal = () => { + setOpenedModalData(null); + }; + const onClose = () => setOpenedModalData(null); + + const openModal = ( + modalType: "MENU" | "SHARE", + username: string, + postId: number + ) => { + setOpenedModalData({ + modalType, + postId: postId, + username: username, + }); + }; + return ( <> = ({ navigation, route }) => { - {route.params.postList && ( - - new Date(b.postedAt).getTime() - - new Date(a.postedAt).getTime() - )} - ItemSeparatorComponent={Divider} - renderItem={({ item }) => } - keyExtractor={(item) => item.postId.toString()} - bouncesZoom - bounces - snapToAlignment={"start"} - showsVerticalScrollIndicator - style={{ - backgroundColor: colors.background, - }} - /> - )} + + + {route.params.postList && ( + + new Date(b.postedAt).getTime() - + new Date(a.postedAt).getTime() + )} + ItemSeparatorComponent={Divider} + renderItem={({ item }) => ( + + )} + keyExtractor={(item) => item.postId.toString()} + bouncesZoom + bounces + snapToAlignment={"start"} + showsVerticalScrollIndicator + style={{ + backgroundColor: colors.background, + }} + /> + )} + ); }; diff --git a/src/app/types/navigation.ts b/src/app/types/navigation.ts index 4040ecb..88c9ff2 100644 --- a/src/app/types/navigation.ts +++ b/src/app/types/navigation.ts @@ -6,12 +6,13 @@ import { definitions } from "./supabase"; import { MaterialTopTabNavigationProp } from "@react-navigation/material-top-tabs"; export type MessageStackNavigationParams = { - MessageList: { + ChatList: { rootNavigation: MaterialTopTabNavigationProp< SwipeTabNavigationParams, "Messages" >; }; + NewChat: undefined; Messages: { username: string; }; @@ -50,7 +51,9 @@ export type HomeStackNavigationParams = { >; }; Comments: CommentsPageParams; - Profile: StackNavigationProp; + Profile: ProfilePageProps & { + goBack: () => void; + }; }; export type ProfileStackParams = { diff --git a/src/app/utils/appContext.tsx b/src/app/utils/appContext.tsx index 0b151b8..0c265d0 100644 --- a/src/app/utils/appContext.tsx +++ b/src/app/utils/appContext.tsx @@ -3,6 +3,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { Theme } from "../types"; type ContextType = { + loading: boolean; signupDone: boolean; setSignupDone: (newState: boolean) => void; @@ -28,6 +29,7 @@ type ContextType = { }; export const AppContext = createContext({ + loading: false, signupDone: false, setSignupDone: () => {}, @@ -56,6 +58,7 @@ type Props = { children: React.ReactNode; }; const AppContextProvider: React.FC = ({ children }) => { + const [loading, setLoading] = useState(true); const [signupDone, setSignupDone] = useState(false); const [username, setUsername] = useState(null); const [name, setName] = useState(null); @@ -100,9 +103,10 @@ const AppContextProvider: React.FC = ({ children }) => { }; useEffect(() => { - AsyncStorage.getItem("signupDone").then((res) => - setSignupDone(res === "true") - ); + AsyncStorage.getItem("signupDone").then((res) => { + setLoading(false); + setSignupDone(res === "true"); + }); AsyncStorage.getItem("username").then((res) => setUsername(res)); AsyncStorage.getItem("email").then((res) => setEmail(res)); AsyncStorage.getItem("bio").then((res) => setBio(res)); @@ -128,6 +132,7 @@ const AppContextProvider: React.FC = ({ children }) => { return ( void; }; -const useMessageList = (): ReturnType => { - const [messageList, setMessageList] = useState>([]); +const useChatList = (): ReturnType => { + const [messageList, setMessageList] = useState>([]); const [loading, setLoading] = useState(true); const { username: currentUser } = useContext(AppContext); - const getUsersList = (messageListData: Message[]) => { - const users = messageListData.map((msg) => ({ - username: msg.sender === currentUser ? msg.receiver : msg.sender, - messageType: msg.message_type, - text: msg.text, - lastMessageAt: msg.received_at, - })); - - return [ - ...new Map(users.map((item) => [item.username, item])).values(), - ]; - }; - const messageListRef = useRef(messageList); useEffect(() => { messageListRef.current = messageList; @@ -100,11 +88,14 @@ const useMessageList = (): ReturnType => { }, [currentUser]); const fetchMessageListData = async () => { + if (!currentUser) + return { + messageList: null, + }; try { const messageListData = (await getMessageListFromDb()) || []; - return { - messageList: getUsersList(messageListData), + messageList: getUsersList(messageListData, currentUser), }; } catch (err) { console.error("[fetchUserData]", err); @@ -127,10 +118,10 @@ const useMessageList = (): ReturnType => { new Date(b.received_at).getTime() - new Date(a.received_at).getTime() ); - if (messageListData) setMessageList(getUsersList(messageListData)); + if (messageListData) + setMessageList(getUsersList(messageListData, currentUser)); setLoading(false); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentUser]); const fetchMessageList = async () => { @@ -160,4 +151,4 @@ const useMessageList = (): ReturnType => { }; }; -export default useMessageList; +export default useChatList; diff --git a/src/app/utils/useCurrentUser.tsx b/src/app/utils/useCurrentUser.tsx index ae0ee7c..24a8b3a 100644 --- a/src/app/utils/useCurrentUser.tsx +++ b/src/app/utils/useCurrentUser.tsx @@ -51,7 +51,7 @@ const useCurrentUser = () => { setCurrentUser(data); } - }, []); + }, [bio, email, name, profilePic, username]); useEffect(() => { if (email && username) { diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index 85256cd..e608849 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -1,7 +1,22 @@ import { format, formatDistanceToNow } from "date-fns"; +import { Message } from "../store/MessagesStore"; import { Post } from "../store/PostsStore"; import { definitions } from "../types/supabase"; +export const getUsersList = ( + messageListData: Message[], + currentUser: string +) => { + const users = messageListData.map((msg) => ({ + username: msg.sender === currentUser ? msg.receiver : msg.sender, + messageType: msg.message_type, + text: msg.text, + lastMessageAt: msg.received_at, + })); + + return [...new Map(users.map((item) => [item.username, item])).values()]; +}; + export function uniqueList( oldList: Model[], newList: Model[], diff --git a/yarn.lock b/yarn.lock index 5810221..d91345a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1548,6 +1548,26 @@ lodash.isequal "^4.5.0" react-native-redash "15.7.3" +"@gorhom/bottom-sheet@^3": + version "3.6.3" + resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-3.6.3.tgz#461c8e419920f39f65a9ffa467f4406a890faee0" + integrity sha512-bFs7q29OyECsrT1GJUMgwBwGgT4OzH5640QWur5ZzJ73u3nQKxAVzGtbtc0JyGCbUPq66iXeQDyAwT3xNkG/iQ== + dependencies: + "@gorhom/portal" "^1.0.4" + invariant "^2.2.4" + lodash.isequal "^4.5.0" + nanoid "^3.1.20" + react-native-redash "^16.0.8" + +"@gorhom/portal@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@gorhom/portal/-/portal-1.0.4.tgz#643c7baaffee223819cdd99dad748c3d9dc6bd73" + integrity sha512-KalM9E6Op1TCzi00YKhczBz86EV0yYpWuRuTt5J4VS09O2gGbGAEEJwUKb/Rvr3XadtinpLjePj0tZ6lFVPp+g== + dependencies: + immer "^8.0.1" + lodash.isequal "^4.5.0" + nanoid "^3.1.20" + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -7040,6 +7060,11 @@ immer@8.0.1: resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== +immer@^8.0.1: + version "8.0.4" + resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.4.tgz#3a21605a4e2dded852fb2afd208ad50969737b7a" + integrity sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ== + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -11191,6 +11216,15 @@ react-native-redash@15.7.3: normalize-svg-path "^1.0.1" parse-svg-path "^0.1.2" +react-native-redash@^16.0.8: + version "16.0.11" + resolved "https://registry.yarnpkg.com/react-native-redash/-/react-native-redash-16.0.11.tgz#973c3050a0645e3b9873790a8e68694770a32f27" + integrity sha512-9yTpeabkAoWASzmhEkynNh1wDTrLVtbE+x9dOV1DI23vRa7k1HI+Y1jOUNOPyRG2ddgHampNq9iHtsLnkK4mtw== + dependencies: + abs-svg-path "^0.1.1" + normalize-svg-path "^1.0.1" + parse-svg-path "^0.1.2" + react-native-safe-area-context@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-3.2.0.tgz#06113c6b208f982d68ab5c3cebd199ca93db6941" @@ -11475,6 +11509,11 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +reanimated-bottom-sheet@^1.0.0-alpha.22: + version "1.0.0-alpha.22" + resolved "https://registry.yarnpkg.com/reanimated-bottom-sheet/-/reanimated-bottom-sheet-1.0.0-alpha.22.tgz#01a200946f1a461f01f1e773e5b4961c2df2e53b" + integrity sha512-NxecCn+2iA4YzkFuRK5/b86GHHS2OhZ9VRgiM4q18AC20YE/psRilqxzXCKBEvkOjP5AaAvY0yfE7EkEFBjTvw== + recast@^0.20.3: version "0.20.4" resolved "https://registry.yarnpkg.com/recast/-/recast-0.20.4.tgz#db55983eac70c46b3fff96c8e467d65ffb4a7abc" @@ -13335,6 +13374,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-memo-one@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" + integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== + use-subscription@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"