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,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);

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 { 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: "<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}) => {
return (
<ScreenWithTrayJsx
@ -61,16 +140,7 @@ const ViewProfileJsx = ({navigation}) => {
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 = (
<TouchableWithoutFeedback>
<TouchableOpacity
onPress = {
() => {
navigation.navigate("Settings");
}
}>
<View style = { styles.button }>
<Text style = { styles.buttonText }>Edit profile</Text>
<Text style = { styles.buttonText }>Settings</Text>
</View>
</TouchableWithoutFeedback>
</TouchableOpacity>
);
} else {
profileButton = (
<TouchableWithoutFeedback>
<TouchableOpacity>
<View style = { styles.button }>
<Text style = { styles.buttonText }>Follow</Text>
</View>
</TouchableWithoutFeedback>
</TouchableOpacity>
)
}
return (
<View>
<View style = { styles.jumbotron }>
<View style = { styles.profileHeader }>
<Image
source = { { uri: state.avatar } }
style = { styles.avatar } />
<View>
<Text
style = { styles.displayName }>
{state.displayName}
</Text>
<Text><Text style={ styles.strong}> @{state.username} </Text></Text>
</View>
<TouchableWithoutFeedback>
<Image
source = { activeOrNot(state.unread_notifs, notif_pack) }
style = { styles.bell } />
</TouchableWithoutFeedback>
</View>
<Text style = { styles.accountStats }>
{ state.statusesCount } posts &#8226;&nbsp;
{ state.followersCount } followers &#8226;&nbsp;
{ state.followingCount } following
</Text>
<Text style = { styles.note }>
{state.note}
</Text>
{profileButton}
</View>
{ state.loaded ?
<>
<View style = { styles.jumbotron }>
<View style = { styles.profileHeader }>
<Image
source = { { uri: state.profile.avatar } }
style = { styles.avatar } />
<View>
<Text
style = { styles.displayName }>
{state.profile.display_name}
</Text>
<Text style={ styles.strong }>
@{state.profile.username }
</Text>
</View>
{
state.own ?
<View style = { styles.profileContextContainer }>
<TouchableOpacity>
<Image
source = { activeOrNot(state.unread_notifs, notif_pack) }
style = { styles.profileHeaderIcon } />
</TouchableOpacity>
</View>
: <ModerateMenuJsx
triggerStyle = { styles.profileHeaderIcon }
containerStyle = { styles.profileContextContainer } />
}
</View>
<Text style = { styles.accountStats }>
{ state.profile.statuses_count } posts &#8226;&nbsp;
<Text onPress = {
() => {
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"}</>
}
<GridViewJsx
posts = { TEST_POSTS }
openPostCallback = {
(id) => {
navigation.navigate("ViewPost", {
id: id,
originTab: "Profile"
});
}
} />
</Text>
</Text>
<Text style = { styles.note }>
{state.profile.note}
</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}
</View>
<GridViewJsx
posts = { TEST_POSTS }
openPostCallback = {
(id) => {
navigation.navigate("ViewPost", {
id: id,
originTab: "Profile"
});
}
} />
</>
: <View></View>
}
</View>
);
};
@ -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;
export default ProfileJsx;

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
} 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 } } />
<Text
style = { styles.postHeaderName }>{ props.data.username }</Text>
<View style = { styles.menu }>
<Menu renderer = { SlideInMenu }>
<MenuTrigger>
<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>
<ModerateMenuJsx
containerStyle = { styles.menu }
triggerStyle = { styles.ellipsis } />
</View>
{
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) {
if (n < 2) {
return singular;