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;