diff --git a/assets/eva-icons/close.png b/assets/eva-icons/close.png
new file mode 100644
index 0000000..e85d20a
Binary files /dev/null and b/assets/eva-icons/close.png differ
diff --git a/assets/eva-icons/plus.png b/assets/eva-icons/plus.png
new file mode 100644
index 0000000..3a7e147
Binary files /dev/null and b/assets/eva-icons/plus.png differ
diff --git a/src/App.js b/src/App.js
index 9b7d6db..173b96a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -15,23 +15,27 @@ import DiscoverJsx from 'src/components/pages/discover';
import SearchJsx from 'src/components/pages/discover/search';
import ViewHashtagJsx from 'src/components/pages/discover/view-hashtag';
import NotificationsJsx from 'src/components/pages/profile/notifications';
+import UserListJsx from "src/components/pages/user-list.js";
+import SettingsJsx from "src/components/pages/profile/settings.js";
const Stack = createStackNavigator({
- Feed: { screen: FeedJsx, },
- Discover: { screen: DiscoverJsx },
- Notifications: { screen: NotificationsJsx },
- Profile: { screen: ProfileJsx, },
- Search: { screen: SearchJsx },
- ViewPost: { screen: ViewPostJsx },
- ViewComments: { screen: ViewCommentsJsx },
- ViewProfile: { screen: ViewProfileJsx },
- ViewHashtag: { screen: ViewHashtagJsx }
+ Feed: { screen: FeedJsx, },
+ Discover: { screen: DiscoverJsx },
+ Notifications: { screen: NotificationsJsx },
+ Profile: { screen: ProfileJsx, },
+ Settings: { screen: SettingsJsx },
+ Search: { screen: SearchJsx },
+ ViewPost: { screen: ViewPostJsx },
+ ViewComments: { screen: ViewCommentsJsx },
+ ViewProfile: { screen: ViewProfileJsx },
+ ViewHashtag: { screen: ViewHashtagJsx },
+ UserList: { screen: UserListJsx }
}, {
- initialRouteKey: "Feed",
- headerMode: "none",
- navigationOptions: {
- headerVisible: false
- }
+ initialRouteKey: "Feed",
+ headerMode: "none",
+ navigationOptions: {
+ headerVisible: false
+ }
});
const App = createAppContainer(Stack);
diff --git a/src/components/moderate-menu.js b/src/components/moderate-menu.js
new file mode 100644
index 0000000..21987ee
--- /dev/null
+++ b/src/components/moderate-menu.js
@@ -0,0 +1,51 @@
+import React from "react";
+import { Dimensions, View, Image } from "react-native";
+import {
+ Menu,
+ MenuOptions,
+ MenuOption,
+ MenuTrigger,
+ renderers
+} from "react-native-popup-menu";
+
+const { SlideInMenu } = renderers;
+
+const SCREEN_WIDTH = Dimensions.get("window").width;
+
+const ModerateMenuJsx = (props) => {
+ const optionsStyles = {
+ optionWrapper: { // The wrapper around a single option
+ paddingLeft: SCREEN_WIDTH / 15,
+ paddingTop: SCREEN_WIDTH / 30,
+ paddingBottom: SCREEN_WIDTH / 30
+ },
+ optionsWrapper: { // The wrapper around all options
+ marginTop: SCREEN_WIDTH / 20,
+ marginBottom: SCREEN_WIDTH / 20,
+ },
+ optionsContainer: { // The Animated.View
+ borderTopLeftRadius: 10,
+ borderTopRightRadius: 10
+ }
+ };
+
+ return (
+
+
+
+ );
+}
+
+export { ModerateMenuJsx as default };
diff --git a/src/components/pages/profile.js b/src/components/pages/profile.js
index ea5c299..1a80d83 100644
--- a/src/components/pages/profile.js
+++ b/src/components/pages/profile.js
@@ -1,7 +1,16 @@
import React, { useState, useEffect } from "react";
-import { View, Dimensions, Image, Text, TouchableWithoutFeedback } from "react-native";
+import {
+ View,
+ Dimensions,
+ Image,
+ Text,
+ TouchableOpacity
+} from "react-native";
-import { activeOrNot } from "src/interface/interactions"
+import * as Linking from "expo-linking";
+
+import { activeOrNot } from "src/interface/interactions";
+import { withoutHTML } from "src/interface/rendering";
import GridViewJsx from "src/components/posts/grid-view";
import {
@@ -9,6 +18,8 @@ import {
ScreenWithFullNavigationJsx
} from "src/components/navigation/navigators";
+import ModerateMenuJsx from "src/components/moderate-menu.js";
+
const TEST_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg";
const TEST_POSTS = [
{
@@ -37,6 +48,74 @@ const TEST_POSTS = [
}
];
+const TEST_PROFILE = {
+ username: "njms",
+ acct: "njms",
+ display_name: "Nat🔆",
+ locked: false,
+ bot: false,
+ note: "Yeah heart emoji.",
+ avatar: TEST_IMAGE,
+ followers_count: "1 jillion",
+ statuses_count: 334,
+ fields: [
+ {
+ name: "Blog",
+ value: "https://njms.ca",
+ verified_at: "some time"
+ },
+ {
+ name: "Github",
+ value: "https://github.com/natjms",
+ verified_at: null
+ }
+ ]
+};
+
+const TEST_YOUR_FOLLOWERS = [
+ { id: 1 },
+ { id: 2 },
+ { id: 3 },
+ { id: 4 },
+ { id: 5 },
+];
+
+const TEST_THEIR_FOLLOWERS = [
+ { id: 2 },
+ { id: 3 },
+ { id: 4 },
+ { id: 6 },
+];
+
+function getMutuals(yours, theirs) {
+ // Where yours and theirs are arrays of followers, as returned by the oAPI
+
+ const idify = ({id}) => id;
+ const asIDs = new Set(theirs.map(idify));
+
+ return yours.filter(x => asIDs.has(idify(x)));
+}
+
+const HTMLLink = ({link}) => {
+ let url = link.match(/https?:\/\/\w+\.\w+/);
+
+ if (url) {
+ return (
+ {
+ Linking.openURL(url[0]);
+ }
+ }>
+ { withoutHTML(link) }
+
+ );
+ } else {
+ return ( { withoutHTML(link) } );
+ }
+}
+
const ProfileJsx = ({navigation}) => {
return (
{
const ProfileDisplayJsx = ({navigation}) => {
const accountName = navigation.getParam("acct", "");
let [state, setState] = useState({
- avatar: "",
- displayName: "Somebody",
- username: "somebody",
- statusesCount: 0,
- followersCount: 0,
- followingCount: 0,
- note: "Not much here...",
- unread_notifications: false,
- own: false,
- loaded: false
+ loaded: false,
});
const notif_pack = {
@@ -80,82 +150,129 @@ const ProfileDisplayJsx = ({navigation}) => {
useEffect(() => {
// do something to get the profile based on given account name
- if (!state.loaded) {
- setState({
- avatar: TEST_IMAGE,
- displayName: "Nat🔆",
- username: "njms",
- statusesCount: 334,
- followersCount: "1 jillion",
- followingCount: 7,
- note: "Yeah heart emoji.",
- own: true,
- unread_notifs: false,
- loaded: true
- });
- }
- });
+ setState({
+ profile: TEST_PROFILE,
+ mutuals: getMutuals(TEST_YOUR_FOLLOWERS, TEST_THEIR_FOLLOWERS),
+ own: true,
+ loaded: true,
+ });
+ }, []);
let profileButton;
if (state.own) {
profileButton = (
-
+ {
+ navigation.navigate("Settings");
+ }
+ }>
- Edit profile
+ Settings
-
+
);
} else {
profileButton = (
-
+
Follow
-
+
)
}
return (
-
-
-
-
-
- {state.displayName}
-
- @{state.username}
-
-
-
-
-
-
- { state.statusesCount } posts •
- { state.followersCount } followers •
- { state.followingCount } following
-
-
- {state.note}
-
- {profileButton}
-
+ { state.loaded ?
+ <>
+
+
+
+
+
+ {state.profile.display_name}
+
+
+ @{state.profile.username }
+
+
+ {
+ state.own ?
+
+
+
+
+
+ :
+ }
+
+
+ { state.profile.statuses_count } posts •
+ {
+ const context = state.own ?
+ "People following you"
+ : "Your mutual followers with " + state.profile.display_name;
+ navigation.navigate("UserList", {
+ data: [/*Some array of users*/],
+ context: context
+ });
+ }
+ }>
+ {
+ state.own ?
+ <>View followers>
+ : <>{state.mutuals.length + " mutuals"}>
+ }
- {
- navigation.navigate("ViewPost", {
- id: id,
- originTab: "Profile"
- });
- }
- } />
+
+
+
+ {state.profile.note}
+
+
+ {
+ state.profile.fields.map((row, i) =>
+
+
+ { row.name }
+ |
+
+
+
+
+ |
+
+ )
+ }
+
+ {profileButton}
+
+
+ {
+ navigation.navigate("ViewPost", {
+ id: id,
+ originTab: "Profile"
+ });
+ }
+ } />
+ >
+ :
+ }
);
};
@@ -171,7 +288,6 @@ const styles = {
display: "flex",
flexDirection: "row",
alignItems: "center",
-
marginBottom: screen_width / 20
},
displayName: {
@@ -184,10 +300,11 @@ const styles = {
borderRadius: "100%",
marginRight: screen_width / 20
},
- bell: {
+ profileHeaderIcon: {
width: screen_width / 12,
height: screen_width / 12,
-
+ },
+ profileContextContainer: {
marginLeft: "auto",
marginRight: screen_width / 15
},
@@ -199,6 +316,23 @@ const styles = {
fontSize: 16,
marginTop: 10,
},
+
+ metaData: {
+ marginTop: 20
+ },
+ row: {
+ padding: 10
+ },
+ rowName: {
+ width: "33%",
+ textAlign: "center"
+ },
+ rowValue: { width: "67%" },
+ anchor: {
+ color: "#888",
+ textDecoration: "underline"
+ },
+
button: {
borderWidth: 1,
borderStyle: "solid",
@@ -217,4 +351,4 @@ const styles = {
};
export { ViewProfileJsx, ProfileDisplayJsx };
-export default ProfileJsx;
\ No newline at end of file
+export default ProfileJsx;
diff --git a/src/components/pages/profile/settings.js b/src/components/pages/profile/settings.js
new file mode 100644
index 0000000..0f6d11a
--- /dev/null
+++ b/src/components/pages/profile/settings.js
@@ -0,0 +1,293 @@
+import React, { useState, useEffect } from "react";
+
+import {
+ SafeAreaView,
+ View,
+ TextInput,
+ Text,
+ Image,
+ TouchableOpacity,
+ Dimensions,
+} from "react-native";
+
+import { withoutHTML } from "src/interface/rendering";
+
+import { ScreenWithBackBarJsx } from "src/components/navigation/navigators";
+
+const TEST_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg";
+const TEST_PROFILE = {
+ username: "njms",
+ acct: "njms",
+ display_name: "Nat🔆",
+ locked: false,
+ bot: false,
+ note: "Yeah heart emoji.",
+ avatar: TEST_IMAGE,
+ followers_count: "1 jillion",
+ statuses_count: 334,
+ fields: [
+ {
+ name: "Blog",
+ value: "https://njms.ca",
+ verified_at: "some time"
+ },
+ {
+ name: "Github",
+ value: "https://github.com/natjms",
+ verified_at: null
+ }
+ ]
+};
+
+const SettingsJsx = (props) => {
+ const [state, setState] = useState({
+ // Use Context to get this stuff eventually
+ profile: TEST_PROFILE,
+ newProfile: TEST_PROFILE,
+ });
+
+ const fields = state.newProfile.fields;
+
+ return (
+
+
+
+
+
+ Change profile photo
+
+
+
+
+ Display name
+ {
+ setState({...state,
+ newProfile: {...state.newProfile, display_name: value}
+ });
+ }
+ }/>
+
+ User name
+ {
+ setState({...state,
+ newProfile: {...state.newProfile, username: value}
+ });
+ }
+ }/>
+
+ Bio
+ {
+ setState({...state,
+ newProfile: {...state.newProfile, note: value}
+ });
+ }
+ }/>
+ {
+ fields.map((field, i) =>
+
+ {
+ let newFields;
+ if (fields.length == 1) {
+ newFields = [{ name: "", value: "" }];
+ } else {
+ newFields = state.newProfile.fields;
+ newFields.splice(i, 1);
+ }
+
+ setState({...state,
+ newProfile: {...state.newProfile,
+ fields: newFields,
+ },
+ });
+ }
+ }>
+
+
+
+ Name
+ {
+ let newFields = fields;
+ newFields[i] = {...newFields[i],
+ name: text,
+ };
+
+ setState({...state,
+ newProfile: {...state.newProfile,
+ fields: newFields,
+ },
+ });
+ }
+ } />
+
+
+ Value
+ {
+ let newFields = fields;
+ newFields[i] = {...newFields[i],
+ value: text,
+ };
+
+ setState({...state,
+ newProfile: {...state.newProfile,
+ fields: newFields,
+ },
+ });
+ }
+ } />
+
+
+ )
+ }
+ {
+ setState({...state,
+ newProfile: {...state.newProfile,
+ fields: state.newProfile.fields.concat({ name: "", value: ""}),
+ },
+ });
+ }
+ }>
+
+
+
+ Save Profile
+
+
+ Log out
+
+
+
+ );
+};
+
+const SCREEN_WIDTH = Dimensions.get("window").width;
+const styles = {
+ label: {
+ paddingTop: 10,
+ fontWeight: "bold",
+ color: "#888",
+ },
+ bar: {
+ borderBottomWidth: 1,
+ borderBottomColor: "#888",
+ padding: 10,
+ },
+ avatar: {
+ container: {
+ paddingTop: 10,
+ paddingBottom: 10,
+ flex: 1,
+ alignItems: "center",
+ },
+ image: {
+ width: SCREEN_WIDTH / 5,
+ height: SCREEN_WIDTH / 5,
+ borderRadius: SCREEN_WIDTH / 10,
+ marginBottom: 10,
+ },
+ change: {
+ fontSize: 18,
+ color: "#888",
+ },
+ },
+ input: {
+ container: {
+ padding: 10,
+ },
+ },
+ fields: {
+ container: {
+ flex: 1,
+ flexDirection: "row",
+ alignItems: "flex-end",
+ },
+ cross: {
+ width: 30,
+ height: 30,
+ marginRight: 10,
+ marginBottom: 10,
+ },
+ plus: {
+ width: 30,
+ height: 30,
+ marginLeft: "auto",
+ marginRight: "auto",
+ marginTop: 10,
+ },
+ subContainer: {
+ flexGrow: 0.5,
+ },
+ cell: {
+ width: SCREEN_WIDTH / 2.5,
+ },
+ },
+ largeButton: {
+ width: SCREEN_WIDTH / 1.2,
+ padding: 15,
+ marginTop: 10,
+ marginBottom: 5,
+ marginLeft: "auto",
+ marginRight: "auto",
+ borderWidth: 1,
+ borderColor: "#888",
+ borderRadius: 5,
+ textAlign: "center",
+ },
+ textWarning: {
+ fontWeight: "bold",
+ textDecorationLine: "underline",
+ },
+};
+
+export default SettingsJsx;
diff --git a/src/components/pages/user-list.js b/src/components/pages/user-list.js
new file mode 100644
index 0000000..74616b1
--- /dev/null
+++ b/src/components/pages/user-list.js
@@ -0,0 +1,145 @@
+import React from "react";
+import {
+ SafeAreaView,
+ View,
+ Image,
+ Text,
+ FlatList,
+ Dimensions,
+ TouchableOpacity,
+} from "react-native";
+
+import { ScreenWithBackBarJsx } from "src/components/navigation/navigators.js";
+import ModerateMenuJsx from "src/components/moderate-menu.js";
+
+const TEST_PROFILE = {
+ username: "njms",
+ acct: "njms",
+ display_name: "Nat🔆",
+ locked: false,
+ bot: false,
+ note: "Yeah heart emoji.",
+ avatar: "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg",
+ followers_count: "1 jillion",
+ statuses_count: 334,
+ fields: [
+ {
+ name: "Blog",
+ value: "https://njms.ca",
+ verified_at: "some time"
+ },
+ {
+ name: "Github",
+ value: "https://github.com/natjms",
+ verified_at: null
+ }
+ ]
+};
+
+const TEST_DATA = [
+ {...TEST_PROFILE, id: 1},
+ {...TEST_PROFILE, id: 2},
+ {...TEST_PROFILE, id: 3},
+ {...TEST_PROFILE, id: 4},
+ {...TEST_PROFILE, id: 5},
+ {...TEST_PROFILE, id: 6}
+]
+
+function renderItemFactory(navigation) {
+ // Returns a renderItem function with the context of props.navigation so
+ // that it can enable the person to navigate to the selected account.
+ return ({item}) => (
+
+ {
+ navigation.navigate("Profile", { acct: item.acct });
+ }
+ }>
+
+
+
+
+ @{ item.acct }
+
+
+ { item.display_name }
+
+
+
+
+
+
+ );
+}
+
+const UserListJsx = ({navigation}) => {
+ // const data = navigation.getParam("data", [])
+ const data = TEST_DATA;
+ const context = navigation.getParam("context", "");
+
+ const renderItem = renderItemFactory(navigation);
+
+ return (
+
+ {
+ context ?
+
+ { context }:
+
+ : <>>
+ }
+ item.id }/>
+
+ );
+};
+
+const SCREEN_WIDTH = Dimensions.get("window").width;
+
+const styles = {
+ context: {
+ fontSize: 18,
+ //color: "#888",
+ padding: 10,
+ },
+ flexContainer: {
+ flex: 1,
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ itemContainer: { padding: 10 },
+ accountButton: {
+ flexGrow: 1,
+ },
+ bottomBorder: {
+ borderBottomWidth: 1,
+ borderBottomColor: "#888",
+ },
+ avatar: {
+ width: SCREEN_WIDTH / 8,
+ height: SCREEN_WIDTH / 8,
+ borderRadius: SCREEN_WIDTH / 16,
+ marginRight: 10,
+ },
+ acct: {
+ fontWeight: "bold",
+ },
+ moderateMenu: {
+ marginLeft: "auto",
+ marginRight: 10,
+ },
+ ellipsis: {
+ width: 20,
+ height: 20,
+ },
+};
+
+export { UserListJsx as default };
diff --git a/src/components/posts/post.js b/src/components/posts/post.js
index 9e38742..52b9b29 100644
--- a/src/components/posts/post.js
+++ b/src/components/posts/post.js
@@ -8,26 +8,15 @@ import {
ScrollView
} from "react-native";
-import {
- Menu,
- MenuOptions,
- MenuOption,
- MenuTrigger,
- renderers
-} from "react-native-popup-menu";
-
-
-import { pluralize, timeToAge } from "src/interface/rendering"
+import { pluralize, timeToAge} from "src/interface/rendering"
import PostActionBarJsx from "src/components/posts/post-action-bar";
+import ModerateMenuJsx from "src/components/moderate-menu.js";
+
const SCREEN_WIDTH = Dimensions.get("window").width;
const TEST_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg";
-// Extract the SlideInMenu function from `renderers`
-// This will be used in RawPostJsx
-const { SlideInMenu } = renderers;
-
function getAutoHeight(w1, h1, w2) {
/*
Given the original dimensions and the new width, calculate what would
@@ -90,20 +79,9 @@ export const RawPostJsx = (props) => {
source = { { uri: props.data.avatar } } />
{ props.data.username }
-
-
-
+
{
props.data.media_attachments.length > 1 ?
diff --git a/src/interface/rendering.js b/src/interface/rendering.js
index 6dee69e..ff2774d 100644
--- a/src/interface/rendering.js
+++ b/src/interface/rendering.js
@@ -1,3 +1,7 @@
+export function withoutHTML(string) {
+ return string.replaceAll(/<[^>]*>/ig, "");
+}
+
export function pluralize(n, singular, plural) {
if (n < 2) {
return singular;