diff --git a/assets/eva-icons/person-black-notif.png b/assets/eva-icons/person-black-notif.png new file mode 100644 index 0000000..95af55d Binary files /dev/null and b/assets/eva-icons/person-black-notif.png differ diff --git a/assets/eva-icons/person-grey-notif.png b/assets/eva-icons/person-grey-notif.png new file mode 100644 index 0000000..755b41e Binary files /dev/null and b/assets/eva-icons/person-grey-notif.png differ diff --git a/package-lock.json b/package-lock.json index a0d6e83..d612379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6137,6 +6137,11 @@ "resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.2.1.tgz", "integrity": "sha512-/VbpIEp8tSNNHIvstuA3Swx610whci1Zpc9mqNkqn14DkMbw+ORviln2u0XyHG1kPvvwTNGZY6QpeFwxYaSdbQ==" }, + "react-native-pager-view": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-5.1.2.tgz", + "integrity": "sha512-UvPvjtuIkiI9Ti8NoMH+fiFj0ehfFv4WkNUGM46dOJfOxmE6Z/hoyJjymOHU//iLkQSMO+YNherZs0HcijdA2A==" + }, "react-native-popup-menu": { "version": "0.15.10", "resolved": "https://registry.npmjs.org/react-native-popup-menu/-/react-native-popup-menu-0.15.10.tgz", @@ -6168,6 +6173,11 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-2.9.0.tgz", "integrity": "sha512-5MaiUD6HA3nzY3JbVI8l3V7pKedtxQF3d8qktTVI0WmWXTI4QzqOU8r8fPVvfKo3MhOXwhWBjr+kQ7DZaIQQeg==" }, + "react-native-tab-view": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-2.16.0.tgz", + "integrity": "sha512-ac2DmT7+l13wzIFqtbfXn4wwfgtPoKzWjjZyrK1t+T8sdemuUvD4zIt+UImg03fu3s3VD8Wh/fBrIdcqQyZJWg==" + }, "react-native-web": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.11.7.tgz", diff --git a/package.json b/package.json index e0fe5c7..831158c 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,12 @@ "react-dom": "~16.11.0", "react-native": "https://github.com/expo/react-native/archive/sdk-38.0.2.tar.gz", "react-native-gesture-handler": "~1.6.0", + "react-native-pager-view": "^5.1.2", "react-native-popup-menu": "^0.15.10", "react-native-reanimated": "~1.9.0", "react-native-safe-area-context": "~3.0.7", "react-native-screens": "~2.9.0", + "react-native-tab-view": "^2.16.0", "react-native-web": "~0.11.7", "react-navigation": "^4.4.0", "react-navigation-stack": "^2.8.2" diff --git a/src/components/navigation/tray.js b/src/components/navigation/tray.js index 5d0c921..831862e 100644 --- a/src/components/navigation/tray.js +++ b/src/components/navigation/tray.js @@ -1,5 +1,8 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { Image } from "react-native"; + +import { checkUnreadNotifications } from "src/requests"; + import { activeOrNot } from "src/interface/interactions" import { TouchableWithoutFeedback, View } from "react-native"; @@ -18,6 +21,19 @@ const TrayButtonJsx = (props) => { const TrayJsx = (props) => { const nav = props.navigation; + const [state, setState] = useState({ + unreadNotifications: false + }); + + useEffect(() => { + checkUnreadNotifications() + .then(isUnread => { + setState({...state, + unreadNotifications: isUnread, + }); + }); + }, []); + const icons = { feed: { @@ -40,8 +56,12 @@ const TrayJsx = (props) => { active: require("assets/eva-icons/person-black.png"), inactive: require("assets/eva-icons/person-grey.png") }, + profileNotif: { + active: require("assets/eva-icons/person-black-notif.png"), + inactive: require("assets/eva-icons/person-grey-notif.png") + }, } - + return ( @@ -67,7 +87,11 @@ const TrayJsx = (props) => { nav = { nav } /> @@ -103,4 +127,4 @@ const styles = { } }; -export default TrayJsx; \ No newline at end of file +export default TrayJsx; diff --git a/src/components/pages/authenticate.js b/src/components/pages/authenticate.js index 1d51162..d10d2ed 100644 --- a/src/components/pages/authenticate.js +++ b/src/components/pages/authenticate.js @@ -44,18 +44,28 @@ const AuthenticateJsx = ({navigation}) => { }); useEffect(() => { - const profile = AsyncStorage.getItem("@user_profile").then((profile) => { - if (profile != null) { - navigation.navigate("feed"); + AsyncStorage.getItem("@user_profile").then((profile) => { + if (profile) { + navigation.navigate("Feed"); } setState({...state, authChecked: true}); }); }, []); - const loginCallback = async () => { - const profileJSON = JSON.stringify(TEST_PROFILE); - AsyncStorage.setItem("@user_profile", profileJSON).then(() => { + const loginCallback = () => { + const initialization = [ + [ "@user_profile", JSON.stringify(TEST_PROFILE) ], + [ + "@user_notifications", + JSON.stringify({ + unread: false, + memory: [{ id: 1 }, { id: 2 }], + }) + ] + ]; + + AsyncStorage.multiSet(initialization).then(() => { navigation.navigate("Feed"); }); }; diff --git a/src/components/pages/discover.js b/src/components/pages/discover.js index d44f68c..097862c 100644 --- a/src/components/pages/discover.js +++ b/src/components/pages/discover.js @@ -1,11 +1,61 @@ import React, { useEffect, useState } from "react"; import { View, TextInput, Text, Dimensions } from "react-native"; +import { TabView, TabBar, SceneMap } from "react-native-tab-view"; + +import { Ionicons } from "@expo/vector-icons"; + import PagedGridJsx from "src/components/posts/paged-grid"; import { ScreenWithTrayJsx } from "src/components/navigation/navigators"; import { TouchableWithoutFeedback } from "react-native-gesture-handler"; const DiscoverJsx = (props) => { + const [index, setIndex] = useState(0); + const [routes] = useState([ + { + key: "home", + icon: "md-home", + }, + { + key: "federated", + icon: "md-planet", + }, + ]); + + const HomeTimeline = () => ( + + ); + + const FederatedTimeline = () => ( + + ); + + const renderScene = SceneMap({ + home: HomeTimeline, + federated: FederatedTimeline, + }); + + const renderTabBar = (props) => ( + + ); + + const renderIcon = ({ route, color }) => ( + + ); + return ( { - + ); }; +const SCREEN_WIDTH = Dimensions.get("window").width; const styles = { form: { display: "flex", @@ -37,8 +91,15 @@ const styles = { searchBar: { padding: 10, fontSize: 17, - color: "#888" + color: "#888", + borderBottomWidth: 1, + borderBottomColor: "#CCC", + }, + + tabBar: { + indicator: { backgroundColor: "black" }, + tab: { backgroundColor: "white" }, }, }; -export default DiscoverJsx; \ No newline at end of file +export default DiscoverJsx; diff --git a/src/components/pages/discover/search.js b/src/components/pages/discover/search.js index 779374d..0592ba2 100644 --- a/src/components/pages/discover/search.js +++ b/src/components/pages/discover/search.js @@ -67,11 +67,11 @@ const SearchJsx = ({navigation}) => { { state.query == "" ? : - Accounts + Accounts - Hashtags + Hashtags @@ -156,6 +156,10 @@ const styles = { fontSize: 17, color: "#888" }, + label: { + padding: 10, + fontSize: 15, + }, searchList: { padding: 0 }, searchResultContainer: { display: "flex", @@ -177,4 +181,4 @@ const styles = { } } -export default SearchJsx; \ No newline at end of file +export default SearchJsx; diff --git a/src/components/pages/discover/view-hashtag.js b/src/components/pages/discover/view-hashtag.js index 48519aa..d095133 100644 --- a/src/components/pages/discover/view-hashtag.js +++ b/src/components/pages/discover/view-hashtag.js @@ -2,15 +2,15 @@ import React, { useState } from "react"; import { View, Image, Dimensions, Text } from "react-native"; import { ScreenWithFullNavigationJsx } from "src/components/navigation/navigators"; import PagedGridJsx from "src/components/posts/paged-grid"; -import { TouchableWithoutFeedback } from "react-native-gesture-handler"; +import { TouchableOpacity } from "react-native-gesture-handler"; const FollowHashtagButtonJsx = ({followed, onPress}) => { return ( - @@ -18,7 +18,7 @@ const FollowHashtagButtonJsx = ({followed, onPress}) => { style = { followed ? { color: "white" } : {} }> { followed ? "Followed" : "Follow" } - + ); }; @@ -43,13 +43,13 @@ const ViewHashtagJsx = ({navigation}) => { - #{ state.name } + #{ state.name } { state.nPosts } posts - { // Send request to follow hashtag and such... @@ -69,34 +69,34 @@ const ViewHashtagJsx = ({navigation}) => { const screen_width = Dimensions.get("window").width; const styles = { headerContainer: { - display: "flex", flexDirection: "row", alignItems: "center", padding: 15, - borderBottom: "2px solid black" }, image: { width: screen_width / 3, height: screen_width / 3, - border: "2px solid black", + borderWidth: 1, + borderColor: "#888", borderRadius: "100%", - marginRight: 20 + marginRight: 20, }, hashtag: { fontWeight: "bold", fontSize: 20 }, button: { - border: "2px solid black", + borderWidth: 1, + borderColor: "#888", borderRadius: 5, padding: 10, paddingLeft: 30, paddingRight: 30, - marginTop: 10 + marginTop: 10, }, strong: { - fontWeight: "bold" + fontWeight: "bold", }, } -export default ViewHashtagJsx; \ No newline at end of file +export default ViewHashtagJsx; diff --git a/src/components/pages/profile.js b/src/components/pages/profile.js index 98362ed..a671a8f 100644 --- a/src/components/pages/profile.js +++ b/src/components/pages/profile.js @@ -150,14 +150,21 @@ const ProfileDisplayJsx = ({navigation}) => { } useEffect(() => { - AsyncStorage.getItem("@user_profile").then((profileJSON) => { - setState({ - profile: JSON.parse(profileJSON), - mutuals: getMutuals(TEST_YOUR_FOLLOWERS, TEST_THEIR_FOLLOWERS), - own: true, - loaded: true, + AsyncStorage.multiGet(["@user_profile", "@user_notifications"]) + .then(values => { + const [profileJSON, notificationsJSON] = values; + + const profile = JSON.parse(profileJSON[1]); + const notifications = JSON.parse(notificationsJSON[1]); + console.log(notifications); + setState({ + profile: profile, + unreadNotifications: notifications.unread, + mutuals: getMutuals(TEST_YOUR_FOLLOWERS, TEST_THEIR_FOLLOWERS), + own: true, + loaded: true, + }); }); - }); }, []); let profileButton; @@ -205,9 +212,19 @@ const ProfileDisplayJsx = ({navigation}) => { { state.own ? - + { + navigation.navigate("Notifications"); + } + }> diff --git a/src/components/pages/profile/notifications.js b/src/components/pages/profile/notifications.js index 94f9034..42bd3bb 100644 --- a/src/components/pages/profile/notifications.js +++ b/src/components/pages/profile/notifications.js @@ -1,14 +1,439 @@ -import React, { useState } from "react"; -import { ScreenWithTrayJsx } from "src/components/navigation/navigators"; +import React, { useState, useEffect } from "react"; + +import { + Dimensions, + View, + TouchableOpacity, + Image, + Text, +} from "react-native"; + +import AsyncStorage from "@react-native-async-storage/async-storage"; + +import { ScreenWithBackBarJsx } from "src/components/navigation/navigators"; + +const TEST_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg"; +const TEST_NOTIFICATIONS = [ + { + id: 1, + type: "follow", + account: { + acct: "njms", + avatar: TEST_IMAGE, + }, + }, + { + id: 2, + type: "follow_request", + account: { + acct: "njms", + avatar: TEST_IMAGE, + }, + }, + { + id: 3, + type: "mention", + account: { + acct: "njms", + avatar: TEST_IMAGE, + }, + status: { + id: 1, + media_attachments: [], + content: "This is a message", + } + }, + { + id: 4, + type: "mention", + account: { + acct: "njms", + avatar: TEST_IMAGE, + }, + status: { + id: 1, + media_attachments: [ + { url: TEST_IMAGE } + ], + content: "This is a message", + } + }, + { + id: 5, + type: "mention", + account: { + acct: "njms", + avatar: TEST_IMAGE, + }, + status: { + id: 1, + media_attachments: [ + { url: TEST_IMAGE } + ], + content: "This is a really really really really really really" + + " really really really really really really long message", + } + }, + { + id: 6, + type: "reblog", + account: { + acct: "njms", + avatar: TEST_IMAGE, + }, + status: { + id: 1, + media_attachments: [ + { url: TEST_IMAGE } + ], + } + }, + { + id: 7, + type: "favourite", + account: { + acct: "njms", + avatar: TEST_IMAGE, + }, + status: { + id: 1, + media_attachments: [ + { url: TEST_IMAGE } + ], + } + }, + { + id: 8, + type: "status", + account: { + acct: "njms", + avatar: TEST_IMAGE, + }, + status: { + id: 1, + media_attachments: [ + { url: TEST_IMAGE } + ], + } + }, +] + +function navigateProfileFactory(nav, acct) { + return () => { + nav.navigate("ViewProfile", { + acct: acct, + }); + }; +} + +function navigatePostFactory(nav, id) { + return () => { + nav.navigate("ViewPost", { + originTab: "Profile", + id: id, + }); + } +} + +function renderNotification(notif, navigation) { + switch(notif.type) { + case "follow": + return + case "follow_request": + return + case "mention": + return + case "reblog": + return + case "favourite": + return + case "status": + return + default: + // We're not expecting polls to be super popular on Pixelfed + return <> + } +} + +const UserTextJsx = (props) => { + return ( + { + props.navigation.navigate("ViewProfile", { + acct: props.acct + }); + } + }> + { props.acct }  + + ); +}; + +const NotificationJsx = (props) => { + return ( + + + + + + + + { props.children } + + { props.button ? + + + { props.buttonLabel } + + + : <> + } + + ); +}; + +const FollowJsx = (props) => { + return ( + + + + has followed you. + + + ); +}; + +const FollowRequestJsx = (props) => { + return ( + console.log("Request accepted") }> + + + has requested to follow you. + + + ); +}; + +const MentionJsx = (props) => { + let uri; + let imageStyle; + let thumbnailCallback; + + if (props.data.status.media_attachments.length > 0) { + // If it's a comment... + uri = props.data.status.media_attachments[0].url; + imageStyle = {}; + thumbnailCallback = navigatePostFactory( + props.navigation, + props.data.status.id + ); + } else { + // If it's a reply to your comment... + uri = props.data.account.avatar; + imageStyle = styles.notif.circularThumbnail; + thumbnailCallback = navigateProfileFactory( + props.navigation, + props.data.account.acct + ); + } + + return ( + + + + mentioned you: + + "{ props.data.status.content }" + + + + ); +}; + +const ReblogJsx = (props) => { + return ( + + + + + shared your post. + + + ); +}; + +const FavouriteJsx = (props) => { + return ( + + + + + liked your post. + + + ); +}; + +const StatusJsx = (props) => { + return ( + + + + just posted. + + + ); +}; const NotificationsJsx = ({navigation}) => { + const [state, setState] = useState({ + loaded: false, + }); + + useEffect(() => { + const read = JSON.stringify({ + unread: false, + memory: [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + ] + }); + + AsyncStorage.mergeItem("@user_notifications", read) + .then(() => { + setState({...state, + notifications: TEST_NOTIFICATIONS, + loaded: true + }) + }); + + }, []); + return ( - - - + + { state.loaded ? + + { + state.notifications.map(notif => + renderNotification(notif, navigation) + ) + } + + : <> + } + ); } -export default NotificationsJsx; \ No newline at end of file +const SCREEN_WIDTH = Dimensions.get("window").width; + +const styles = { + notif: { + container: { + flexDirection: "row", + alignItems: "center", + paddingLeft: 20, + marginTop: 10, + marginBottom: 10, + }, + + circularThumbnail: { borderRadius: SCREEN_WIDTH / 16 }, + thumbnailContainer: { + marginRight: 10, + }, + thumbnail: { + width: SCREEN_WIDTH / 8, + height: SCREEN_WIDTH / 8, + }, + + contentContainer: { + flexShrink: 1, + flexDirection: "row", + alignItems: "center", + }, + inlineIcon: { + width: 20, + height: 20, + marginRight: 10, + }, + status: { fontStyle: "italic" }, + + buttonContainer: { + marginLeft: "auto", + marginRight: 10, + }, + button: { + borderWidth: 1, + borderColor: "#888", + borderRadius: 10, + padding: 10, + }, + }, + bold: { fontWeight: "bold" }, +}; + +export default NotificationsJsx; diff --git a/src/components/pages/profile/settings.js b/src/components/pages/profile/settings.js index 0f6d11a..698cf3a 100644 --- a/src/components/pages/profile/settings.js +++ b/src/components/pages/profile/settings.js @@ -10,6 +10,8 @@ import { Dimensions, } from "react-native"; +import AsyncStorage from "@react-native-async-storage/async-storage"; + import { withoutHTML } from "src/interface/rendering"; import { ScreenWithBackBarJsx } from "src/components/navigation/navigators"; @@ -203,7 +205,17 @@ const SettingsJsx = (props) => { Save Profile - + { + AsyncStorage.multiRemove( + ["@user_profile", "@user_notifications"] + ).then(() => { + props.navigation.navigate("Authenticate"); + }); + } + }> Log out diff --git a/src/components/pages/view-post.js b/src/components/pages/view-post.js index 5cc87ce..481ba1b 100644 --- a/src/components/pages/view-post.js +++ b/src/components/pages/view-post.js @@ -3,14 +3,20 @@ import React from "react"; import { ScreenWithFullNavigationJsx } from "src/components/navigation/navigators"; import { PostByIdJsx } from "src/components/posts/post"; -const ViewPostJsx = (props) => { +const ViewPostJsx = ({navigation}) => { + const id = navigation.getParam("id", undefined); + + if (id == undefined) { + throw Error("ID not specified when navigating to ViewPost!"); + } + return ( + active = { navigation.getParam("originTab", "Timeline") } + navigation = { navigation }> + navigation = { navigation } + id = { id } /> ); } diff --git a/src/components/posts/grid-view.js b/src/components/posts/grid-view.js index e992582..0e9e3e0 100644 --- a/src/components/posts/grid-view.js +++ b/src/components/posts/grid-view.js @@ -1,7 +1,7 @@ import React from "react"; import { View, Dimensions, Image } from "react-native"; -import GridPostJsx from "src/components/posts/grid-post" +import GridPostJsx from "src/components/posts/grid-post"; function partition(arr, size) { let newArray = []; @@ -17,7 +17,7 @@ const GridViewJsx = (props) => { let rows = partition(props.posts, 3); return ( - { + { rows.map((row, i) => { return ( { return ( - { @@ -60,16 +60,16 @@ const PagedGridJSX = (props) => { } } /> - { // TODO: actually get more posts :) let morePosts = state.posts.concat(TEST_POSTS); - setState({ posts: morePosts, loaded: true }); + setState({...state, posts: morePosts}); } }> Show more? - + ); @@ -82,7 +82,8 @@ const styles = { alignItems: "center" }, buttonMore: { - border: "2px solid black", + borderWidth: 1, + borderColor: "#888", borderRadius: 5, padding: 10, margin: 20 diff --git a/src/requests.js b/src/requests.js new file mode 100644 index 0000000..9e8349a --- /dev/null +++ b/src/requests.js @@ -0,0 +1,30 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; + +const TEST_NOTIFICATIONS = [{ id: 1 }, { id: 2 }]; +const TEST_NEW_NOTIFICATIONS_1 = [{ id: 1 }, { id: 2 }]; +const TEST_NEW_NOTIFICATIONS_2 = [{ id: 1 }, { id: 2 }, { id: 3 }]; + +export async function checkUnreadNotifications() { + // If the check has already been made since the last time notifications.js + // has been opened + const notifications = JSON.parse(await AsyncStorage.getItem("@user_notifications")); + + if (notifications.unread) { + return true; + } else { + // Some promise to get new notifications + const newNotifs = await Promise.resolve(TEST_NEW_NOTIFICATIONS_2); + + const isUnread = JSON.stringify(newNotifs) != JSON.stringify(notifications.memory); + + // Update stored notifications + await AsyncStorage.setItem( + "@user_notifications", + JSON.stringify({...notifications, + unread: isUnread, + }) + ); + + return isUnread; + } +}