Merge pull request #10 from natjms/profile

Implementing profiles
This commit is contained in:
Nat 2021-03-27 15:26:25 -03:00 committed by GitHub
commit 00d21265cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 725 additions and 116 deletions

BIN
assets/eva-icons/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
assets/eva-icons/plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -15,17 +15,21 @@ import DiscoverJsx from 'src/components/pages/discover';
import SearchJsx from 'src/components/pages/discover/search'; import SearchJsx from 'src/components/pages/discover/search';
import ViewHashtagJsx from 'src/components/pages/discover/view-hashtag'; import ViewHashtagJsx from 'src/components/pages/discover/view-hashtag';
import NotificationsJsx from 'src/components/pages/profile/notifications'; 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({ const Stack = createStackNavigator({
Feed: { screen: FeedJsx, }, Feed: { screen: FeedJsx, },
Discover: { screen: DiscoverJsx }, Discover: { screen: DiscoverJsx },
Notifications: { screen: NotificationsJsx }, Notifications: { screen: NotificationsJsx },
Profile: { screen: ProfileJsx, }, Profile: { screen: ProfileJsx, },
Settings: { screen: SettingsJsx },
Search: { screen: SearchJsx }, Search: { screen: SearchJsx },
ViewPost: { screen: ViewPostJsx }, ViewPost: { screen: ViewPostJsx },
ViewComments: { screen: ViewCommentsJsx }, ViewComments: { screen: ViewCommentsJsx },
ViewProfile: { screen: ViewProfileJsx }, ViewProfile: { screen: ViewProfileJsx },
ViewHashtag: { screen: ViewHashtagJsx } ViewHashtag: { screen: ViewHashtagJsx },
UserList: { screen: UserListJsx }
}, { }, {
initialRouteKey: "Feed", initialRouteKey: "Feed",
headerMode: "none", headerMode: "none",

View File

@ -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 (
<View style = { props.containerStyle }>
<Menu renderer = { SlideInMenu }>
<MenuTrigger>
<Image
source = { require("assets/eva-icons/ellipsis.png") }
style = { props.triggerStyle }/>
</MenuTrigger>
<MenuOptions customStyles = { optionsStyles }>
<MenuOption text="Hide" />
<MenuOption text="Unfollow" />
<MenuOption text="Mute" />
<MenuOption text="Block" />
</MenuOptions>
</Menu>
</View>
);
}
export { ModerateMenuJsx as default };

View File

@ -1,7 +1,16 @@
import React, { useState, useEffect } from "react"; 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 GridViewJsx from "src/components/posts/grid-view";
import { import {
@ -9,6 +18,8 @@ import {
ScreenWithFullNavigationJsx ScreenWithFullNavigationJsx
} from "src/components/navigation/navigators"; } 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_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg";
const TEST_POSTS = [ 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: "<a href=\"https://njms.ca\">https://njms.ca</a>",
verified_at: "some time"
},
{
name: "Github",
value: "<a href=\"https://github.com/natjms\">https://github.com/natjms</a>",
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 (
<Text
style = { styles.anchor }
onPress = {
() => {
Linking.openURL(url[0]);
}
}>
{ withoutHTML(link) }
</Text>
);
} else {
return (<Text> { withoutHTML(link) } </Text>);
}
}
const ProfileJsx = ({navigation}) => { const ProfileJsx = ({navigation}) => {
return ( return (
<ScreenWithTrayJsx <ScreenWithTrayJsx
@ -61,16 +140,7 @@ const ViewProfileJsx = ({navigation}) => {
const ProfileDisplayJsx = ({navigation}) => { const ProfileDisplayJsx = ({navigation}) => {
const accountName = navigation.getParam("acct", ""); const accountName = navigation.getParam("acct", "");
let [state, setState] = useState({ let [state, setState] = useState({
avatar: "", loaded: false,
displayName: "Somebody",
username: "somebody",
statusesCount: 0,
followersCount: 0,
followingCount: 0,
note: "Not much here...",
unread_notifications: false,
own: false,
loaded: false
}); });
const notif_pack = { const notif_pack = {
@ -80,69 +150,113 @@ const ProfileDisplayJsx = ({navigation}) => {
useEffect(() => { useEffect(() => {
// do something to get the profile based on given account name // do something to get the profile based on given account name
if (!state.loaded) {
setState({ setState({
avatar: TEST_IMAGE, profile: TEST_PROFILE,
displayName: "Nat🔆", mutuals: getMutuals(TEST_YOUR_FOLLOWERS, TEST_THEIR_FOLLOWERS),
username: "njms",
statusesCount: 334,
followersCount: "1 jillion",
followingCount: 7,
note: "Yeah heart emoji.",
own: true, own: true,
unread_notifs: false, loaded: true,
loaded: true
});
}
}); });
}, []);
let profileButton; let profileButton;
if (state.own) { if (state.own) {
profileButton = ( profileButton = (
<TouchableWithoutFeedback> <TouchableOpacity
onPress = {
() => {
navigation.navigate("Settings");
}
}>
<View style = { styles.button }> <View style = { styles.button }>
<Text style = { styles.buttonText }>Edit profile</Text> <Text style = { styles.buttonText }>Settings</Text>
</View> </View>
</TouchableWithoutFeedback> </TouchableOpacity>
); );
} else { } else {
profileButton = ( profileButton = (
<TouchableWithoutFeedback> <TouchableOpacity>
<View style = { styles.button }> <View style = { styles.button }>
<Text style = { styles.buttonText }>Follow</Text> <Text style = { styles.buttonText }>Follow</Text>
</View> </View>
</TouchableWithoutFeedback> </TouchableOpacity>
) )
} }
return ( return (
<View> <View>
{ state.loaded ?
<>
<View style = { styles.jumbotron }> <View style = { styles.jumbotron }>
<View style = { styles.profileHeader }> <View style = { styles.profileHeader }>
<Image <Image
source = { { uri: state.avatar } } source = { { uri: state.profile.avatar } }
style = { styles.avatar } /> style = { styles.avatar } />
<View> <View>
<Text <Text
style = { styles.displayName }> style = { styles.displayName }>
{state.displayName} {state.profile.display_name}
</Text>
<Text style={ styles.strong }>
@{state.profile.username }
</Text> </Text>
<Text><Text style={ styles.strong}> @{state.username} </Text></Text>
</View> </View>
<TouchableWithoutFeedback> {
state.own ?
<View style = { styles.profileContextContainer }>
<TouchableOpacity>
<Image <Image
source = { activeOrNot(state.unread_notifs, notif_pack) } source = { activeOrNot(state.unread_notifs, notif_pack) }
style = { styles.bell } /> style = { styles.profileHeaderIcon } />
</TouchableWithoutFeedback> </TouchableOpacity>
</View>
: <ModerateMenuJsx
triggerStyle = { styles.profileHeaderIcon }
containerStyle = { styles.profileContextContainer } />
}
</View> </View>
<Text style = { styles.accountStats }> <Text style = { styles.accountStats }>
{ state.statusesCount } posts &#8226;&nbsp; { state.profile.statuses_count } posts &#8226;&nbsp;
{ state.followersCount } followers &#8226;&nbsp; <Text onPress = {
{ state.followingCount } following () => {
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"}</>
}
</Text>
</Text> </Text>
<Text style = { styles.note }> <Text style = { styles.note }>
{state.note} {state.profile.note}
</Text> </Text>
<table style = { styles.metaData }>
{
state.profile.fields.map((row, i) =>
<tr
key = { i }
style = { styles.row }>
<td style = { styles.rowName } >
<Text>{ row.name }</Text>
</td>
<td style = { styles.rowValue }>
<Text>
<HTMLLink
link = { row.value } />
</Text>
</td>
</tr>
)
}
</table>
{profileButton} {profileButton}
</View> </View>
@ -156,6 +270,9 @@ const ProfileDisplayJsx = ({navigation}) => {
}); });
} }
} /> } />
</>
: <View></View>
}
</View> </View>
); );
}; };
@ -171,7 +288,6 @@ const styles = {
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
marginBottom: screen_width / 20 marginBottom: screen_width / 20
}, },
displayName: { displayName: {
@ -184,10 +300,11 @@ const styles = {
borderRadius: "100%", borderRadius: "100%",
marginRight: screen_width / 20 marginRight: screen_width / 20
}, },
bell: { profileHeaderIcon: {
width: screen_width / 12, width: screen_width / 12,
height: screen_width / 12, height: screen_width / 12,
},
profileContextContainer: {
marginLeft: "auto", marginLeft: "auto",
marginRight: screen_width / 15 marginRight: screen_width / 15
}, },
@ -199,6 +316,23 @@ const styles = {
fontSize: 16, fontSize: 16,
marginTop: 10, marginTop: 10,
}, },
metaData: {
marginTop: 20
},
row: {
padding: 10
},
rowName: {
width: "33%",
textAlign: "center"
},
rowValue: { width: "67%" },
anchor: {
color: "#888",
textDecoration: "underline"
},
button: { button: {
borderWidth: 1, borderWidth: 1,
borderStyle: "solid", borderStyle: "solid",

View File

@ -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: "<a href=\"https://njms.ca\">https://njms.ca</a>",
verified_at: "some time"
},
{
name: "Github",
value: "<a href=\"https://github.com/natjms\">https://github.com/natjms</a>",
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 (
<ScreenWithBackBarJsx navigation = { props.navigation }>
<View style = { styles.avatar.container }>
<Image
source = { { uri: state.profile.avatar } }
style = { styles.avatar.image }/>
<TouchableOpacity>
<Text style = { styles.avatar.change }>
Change profile photo
</Text>
</TouchableOpacity>
</View>
<View style = { styles.input.container }>
<Text style = { styles.label }>Display name</Text>
<TextInput
style = { styles.bar }
placeholder = { "Display name" }
value = { state.newProfile.display_name }
onChangeText = {
(value) => {
setState({...state,
newProfile: {...state.newProfile, display_name: value}
});
}
}/>
<Text style = { styles.label }>User name</Text>
<TextInput
style = { styles.bar }
placeholder = { "User name" }
value = { state.newProfile.username }
onChangeText = {
(value) => {
setState({...state,
newProfile: {...state.newProfile, username: value}
});
}
}/>
<Text style = { styles.label }>Bio</Text>
<TextInput
style = {
[
styles.bar,
{ height: 100 },
]
}
multiline = { true }
placeholder = { "Bio" }
value = { withoutHTML(state.newProfile.note) }
onChangeText = {
(value) => {
setState({...state,
newProfile: {...state.newProfile, note: value}
});
}
}/>
{
fields.map((field, i) =>
<View
style = { styles.fields.container }
key = { i }>
<TouchableOpacity
onPress = {
() => {
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,
},
});
}
}>
<Image
style = {
[
styles.fields.cross,
fields.length == 1
&& fields[0].name == ""
&& fields[0].value == ""
? { visibility: "hidden" }
: {}
]
}
source = { require("assets/eva-icons/close.png") }/>
</TouchableOpacity>
<View style = { styles.fields.subContainer }>
<Text style = { styles.label }>Name</Text>
<TextInput
style = { [styles.bar, styles.fields.cell] }
placeholder = { "Name" }
value = { withoutHTML(fields[i].name) }
onChangeText = {
(text) => {
let newFields = fields;
newFields[i] = {...newFields[i],
name: text,
};
setState({...state,
newProfile: {...state.newProfile,
fields: newFields,
},
});
}
} />
</View>
<View style = { styles.fields.subContainer }>
<Text style = { styles.label }>Value</Text>
<TextInput
style = { [styles.bar, styles.fields.cell] }
placeholder = { "Value" }
value = { withoutHTML(fields[i].value) }
onChangeText = {
(text) => {
let newFields = fields;
newFields[i] = {...newFields[i],
value: text,
};
setState({...state,
newProfile: {...state.newProfile,
fields: newFields,
},
});
}
} />
</View>
</View>
)
}
<TouchableOpacity
onPress = {
() => {
setState({...state,
newProfile: {...state.newProfile,
fields: state.newProfile.fields.concat({ name: "", value: ""}),
},
});
}
}>
<Image
style = { styles.fields.plus }
source = { require("assets/eva-icons/plus.png") } />
</TouchableOpacity>
<TouchableOpacity style = { styles.largeButton }>
<Text> Save Profile </Text>
</TouchableOpacity>
<TouchableOpacity style = { styles.largeButton }>
<Text style = { styles.textWarning }> Log out </Text>
</TouchableOpacity>
</View>
</ScreenWithBackBarJsx>
);
};
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;

View File

@ -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: "<a href=\"https://njms.ca\">https://njms.ca</a>",
verified_at: "some time"
},
{
name: "Github",
value: "<a href=\"https://github.com/natjms\">https://github.com/natjms</a>",
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}) => (
<View style = { [ styles.flexContainer, styles.itemContainer ] }>
<TouchableOpacity
style = { styles.accountButton }
onPress = {
() => {
navigation.navigate("Profile", { acct: item.acct });
}
}>
<View style = { styles.flexContainer }>
<Image
source = { { uri: item.avatar} }
style = { styles.avatar } />
<View>
<Text style = { styles.acct }>
@{ item.acct }
</Text>
<Text style = { styles.displayName }>
{ item.display_name }
</Text>
</View>
</View>
</TouchableOpacity>
<ModerateMenuJsx
containerStyle = { styles.moderateMenu }
triggerStyle = { styles.ellipsis } />
</View>
);
}
const UserListJsx = ({navigation}) => {
// const data = navigation.getParam("data", [])
const data = TEST_DATA;
const context = navigation.getParam("context", "");
const renderItem = renderItemFactory(navigation);
return (
<ScreenWithBackBarJsx navigation = { navigation }>
{
context ?
<Text style = { styles.context }>
{ context }:
</Text>
: <></>
}
<FlatList
data = { data }
renderItem = { renderItem }
keyExtractor = { item => item.id }/>
</ScreenWithBackBarJsx>
);
};
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 };

View File

@ -8,26 +8,15 @@ import {
ScrollView ScrollView
} from "react-native"; } 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 PostActionBarJsx from "src/components/posts/post-action-bar";
import ModerateMenuJsx from "src/components/moderate-menu.js";
const SCREEN_WIDTH = Dimensions.get("window").width; const SCREEN_WIDTH = Dimensions.get("window").width;
const TEST_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg"; 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) { function getAutoHeight(w1, h1, w2) {
/* /*
Given the original dimensions and the new width, calculate what would Given the original dimensions and the new width, calculate what would
@ -90,20 +79,9 @@ export const RawPostJsx = (props) => {
source = { { uri: props.data.avatar } } /> source = { { uri: props.data.avatar } } />
<Text <Text
style = { styles.postHeaderName }>{ props.data.username }</Text> style = { styles.postHeaderName }>{ props.data.username }</Text>
<View style = { styles.menu }> <ModerateMenuJsx
<Menu renderer = { SlideInMenu }> containerStyle = { styles.menu }
<MenuTrigger> triggerStyle = { styles.ellipsis } />
<Image
source = { require("assets/eva-icons/ellipsis.png") }
style = { styles.ellipsis }/>
</MenuTrigger>
<MenuOptions customStyles = { optionsStyles }>
<MenuOption text="Hide" />
<MenuOption text="Unfollow" />
<MenuOption text="Block" />
</MenuOptions>
</Menu>
</View>
</View> </View>
{ {
props.data.media_attachments.length > 1 ? props.data.media_attachments.length > 1 ?

View File

@ -1,3 +1,7 @@
export function withoutHTML(string) {
return string.replaceAll(/<[^>]*>/ig, "");
}
export function pluralize(n, singular, plural) { export function pluralize(n, singular, plural) {
if (n < 2) { if (n < 2) {
return singular; return singular;