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