Create ViewComments page to display post context

This commit is contained in:
Nat 2021-03-13 07:24:51 -04:00
parent 09424ac4eb
commit 4d458734ca
10 changed files with 430 additions and 51 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -6,9 +6,11 @@ import { createStackNavigator } from "react-navigation-stack";
import { registerRootComponent } from 'expo'; import { registerRootComponent } from 'expo';
import ViewPostJsx from "src/components/pages/view-post";
import ViewCommentsJsx from "src/components/pages/view-comments.js";
import FeedJsx from "src/components/pages/feed"; import FeedJsx from "src/components/pages/feed";
import ProfileJsx, { ViewProfileJsx } from "src/components/pages/profile"; import ProfileJsx, { ViewProfileJsx } from "src/components/pages/profile";
import ViewPostJsx from "src/components/pages/view-post";
import DiscoverJsx from 'src/components/pages/discover'; 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';
@ -21,9 +23,10 @@ const Stack = createStackNavigator({
Profile: { screen: ProfileJsx, }, Profile: { screen: ProfileJsx, },
Search: { screen: SearchJsx }, Search: { screen: SearchJsx },
ViewPost: { screen: ViewPostJsx }, ViewPost: { screen: ViewPostJsx },
ViewComments: { screen: ViewCommentsJsx },
ViewProfile: { screen: ViewProfileJsx }, ViewProfile: { screen: ViewProfileJsx },
ViewHashtag: { screen: ViewHashtagJsx } ViewHashtag: { screen: ViewHashtagJsx }
}, { }, {
initialRouteKey: "Feed", initialRouteKey: "Feed",
headerMode: "none", headerMode: "none",
navigationOptions: { navigationOptions: {

View File

@ -9,7 +9,7 @@ import TrayJsx from "src/components/navigation/tray";
// Provider for context menus // Provider for context menus
// Allows for establishing global styling of context menus // Allows for establishing global styling of context menus
const ContextJsx = (props) => { export const ContextJsx = (props) => {
return ( return (
<MenuProvider customStyles = { providerStyles }> <MenuProvider customStyles = { providerStyles }>
{ props.children } { props.children }

View File

@ -43,7 +43,9 @@ const FeedJsx = (props) => {
<ScreenWithTrayJsx <ScreenWithTrayJsx
active = "Feed" active = "Feed"
navigation = { props.navigation }> navigation = { props.navigation }>
<TimelineViewJsx posts = { TEST_POSTS } /> <TimelineViewJsx
navigation = { props.navigation }
posts = { TEST_POSTS } />
<div style = { styles.interruptionOuter }> <div style = { styles.interruptionOuter }>
<View style = { styles.interruption }> <View style = { styles.interruption }>
<Image <Image

View File

@ -0,0 +1,361 @@
import React, { useEffect, useState } from "react";
import { Dimensions, View, Image, TextInput, Text } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import { timeToAge } from "src/interface/rendering";
import { activeOrNot } from "src/interface/interactions";
import TimelineViewJsx from "src/components/posts/timeline-view";
import { ContextJsx } from "src/components/navigation/navigators";
import BackBarJsx from "src/components/navigation/back-bar";
import { TouchableWithoutFeedback } from "react-native-gesture-handler";
const TEST_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg";
const TEST_CONTEXT = {
ancestors: [],
descendants: [
{
id: "1",
in_reply_to_id: "0",
username: "respondant1",
avatar: TEST_IMAGE,
content: "This is a comment",
favourited: false,
created_at: 1596745156000
},
{
id: "2",
in_reply_to_id: "0",
username: "respondant2",
avatar: TEST_IMAGE,
content: "This is a comment",
favourited: true,
created_at: 1596745156000
},
{
id: "3",
in_reply_to_id: "2",
username: "respondant3",
avatar: TEST_IMAGE,
content: "This is a comment",
favourited: false,
created_at: 1596745156000
},
{
id: "4",
in_reply_to_id: "2",
username: "respondant2",
avatar: TEST_IMAGE,
content: "This is a comment",
favourited: false,
created_at: 1596745156000
},
{
id: "5",
in_reply_to_id: "1",
username: "respondant4",
avatar: TEST_IMAGE,
content: "This is a comment",
favourited: false,
created_at: 1596745156000
},
{
id: "6",
in_reply_to_id: "4",
username: "respondant5",
avatar: TEST_IMAGE,
content: "This is a comment",
favourited: false,
created_at: 1596745156000
},
]
}
function chunkWhile(arr, fun) {
/*
* Chunk a list into partitions while fun returns something truthy
* > chunkWhile([1,1,1,2,2], (a, b) => a == b)
* [[1,1,1], [2,2]]
*/
let parts;
if (arr == []) {
return []
} else {
parts = [[arr[0]]];
}
let tail = arr.slice(1);
if (tail == []) {
return parts;
}
for (let i = 0; i < tail.length; i++) {
let lastPart = parts[parts.length - 1];
if (fun(tail[i], lastPart[lastPart.length - 1])) {
// If fun returns something truthy, push tail[i] to the end of the
// partition at the end of the new array.
parts[parts.length - 1].push(tail[i])
} else {
// Create a new partition starting with tail[i]
parts.push([tail[i]])
}
}
return parts;
}
function threadify(descendants, parentID) {
/*
* Take a list of descendants and sort them into a 2D matrix.
* The first item is the direct descendant of parentID post and the rest
* are all the descendants of the direct descendant in order of id, the
* way Instagram displays conversations in comments.
* i.e. [[first level comment, ...descendants]]
*/
if (descendants == []) {
return [];
}
// Sort comments in order of increasing reply id
const comments = descendants.sort((first, second) => {
return first.in_reply_to_id - second.in_reply_to_id;
});
// Return partitions of comments based on their reply id
const byReply = chunkWhile(comments, (a, b) => {
return a.in_reply_to_id == b.in_reply_to_id;
});
// Start with just the first level comments.
// All these elements should be in singleton arrays so they can be
// appended to.
let sorted = byReply[0].map(x => [x]);
let sub = byReply.slice(1); // All sub-comments
// Repeate the procedure until sub is empty (i.e all comments have been
// sorted)
while (sub.length > 0) {
sorted.forEach((thread, threadIndex) => {
for (let i = 0; i < thread.length; i++) {
const id = thread[i].id;
// Search for comment groups with that id
for(let subIndex = 0; subIndex < sub.length; subIndex++) {
// All items in each partition should have the same reply id
if(id == sub[subIndex][0].in_reply_to_id) {
// Move the newly found thread contents to thread in
// sorted
sorted[threadIndex] = sorted[threadIndex].concat(sub[subIndex]);
sub.splice(subIndex, 1);
}
}
}
});
}
return sorted;
}
const CommentJsx = (props) => {
const packs = {
favourited: {
active: require("assets/eva-icons/post-actions/heart-active.png"),
inactive: require("assets/eva-icons/post-actions/heart-inactive.png")
}
};
return (
<View style = { styles.container }>
<Image
source = { { uri: props.data.avatar } }
style = { styles.avatar } />
<View style = { styles.contentContainer }>
<Text style = { styles.content }>
<span style = { styles.bold }>{ props.data.username }</span>&nbsp;
{ props.data.content }
</Text>
<View style = { styles.commentActions }>
<View>
<Text style = { styles.actionText }>
{ timeToAge((new Date()).getTime(), props.data.created_at) }
</Text>
</View>
<TouchableWithoutFeedback>
<View>
<Text style = { [styles.actionText] }>
Reply
</Text>
</View>
</TouchableWithoutFeedback>
<TouchableWithoutFeedback>
<Image
style = { [styles.heart, styles.action] }
source = { activeOrNot(props.data.favourited, packs.favourited) } />
</TouchableWithoutFeedback>
</View>
</View>
</View>
);
}
const ViewCommentsJsx = (props) => {
let [state, setState] = useState({
postData: undefined,
loaded: false,
reply: ""
});
useEffect(() => {
(() => { // Some magical function that will get all the data needed
setState({ ...state,
descendants: threadify(TEST_CONTEXT.descendants),
postData: props.navigation.getParam("postData"),
loaded: true,
});
})();
}, []);
return (
<ContextJsx>
<View style = { { flex: 1 } }>
<BackBarJsx navigation = { props.navigation }/>
<ScrollView>
{ state.loaded ?
<View style = { { display: state.loaded ? "block" : "none" } }>
<View style = { styles.parentPost }>
<CommentJsx
data = { state.postData } />
</View>
<View>
{
state.descendants.map((thread, i) => {
const comment = thread[0];
const subs = thread.slice(1);
return (
<View key = { i }>
<CommentJsx data = { comment }/>
{
subs.map((sub, j) => {
return (
<View
key = { j }
style = { styles.sub }>
<CommentJsx
data = { sub }/>
</View>
)
})
}
</View>
);
})
}
</View>
</View>
: <View></View>
}
</ScrollView>
<View style = { styles.commentForm }>
<Image
style = { styles.avatar }
source = { { uri: TEST_IMAGE } }/>
<TextInput
style = { styles.commentInput }
placeholder = "Say something..."
multiline = { true }
onChangeText = { c => setState({...state, reply: c }) }/>
<View style = { styles.submitContainer }>
<TouchableWithoutFeedback>
<Image
style = { styles.commentSubmit }
source = { require("assets/eva-icons/paper-plane.png") }/>
</TouchableWithoutFeedback>
</View>
</View>
</View>
</ContextJsx>
);
}
const SCREEN_WIDTH = Dimensions.get("window").width;
const styles = {
bold: {
fontWeight: "bold",
},
container: {
display: "flex",
flexDirection: "row",
flexShrink: 1,
marginTop: 10,
marginBottom: 10,
marginRight: 20,
},
avatar: {
marginLeft: 20,
marginRight: 20,
width: 50,
height: 50,
borderRadius: "100%"
},
contentContainer: {
flexShrink: 1
},
parentPost: {
borderBottomWidth: 1,
borderBottomColor: "#CCC",
marginBottom: 10
},
sub: {
marginLeft: SCREEN_WIDTH / 8
},
commentActions: {
display: "flex",
flexDirection: "row",
alignItems: "center",
},
actionText: {
fontSize: 13,
color: "#666",
paddingRight: 10
},
heart: {
width: 15,
height: 15
},
commentForm: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "white",
borderTopWidth: 1,
borderTopColor: "#CCC",
paddingTop: 10,
paddingBottom: 10
},
commentInput: {
borderWidth: 0,
padding: 10,
flexGrow: 3,
marginRight: 20
},
submitContainer: {
marginLeft: "auto",
marginRight: 20
},
commentSubmit: {
width: 30,
height: 30,
}
};
export default ViewCommentsJsx;

View File

@ -8,9 +8,11 @@ const ViewPostJsx = (props) => {
<ScreenWithFullNavigationJsx <ScreenWithFullNavigationJsx
active = { props.navigation.getParam("originTab", "Timeline") } active = { props.navigation.getParam("originTab", "Timeline") }
navigation = { props.navigation }> navigation = { props.navigation }>
<PostByIdJsx id = { props.id } /> <PostByIdJsx
navigation = { props.navigation }
id = { props.id } />
</ScreenWithFullNavigationJsx> </ScreenWithFullNavigationJsx>
); );
} }
export default ViewPostJsx; export default ViewPostJsx;

View File

@ -89,4 +89,4 @@ const styles = {
} }
} }
export default PagedGridJSX; export default PagedGridJSX;

View File

@ -9,6 +9,8 @@ import {
renderers renderers
} from "react-native-popup-menu"; } from "react-native-popup-menu";
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";
const SCREEN_WIDTH = Dimensions.get("window").width; const SCREEN_WIDTH = Dimensions.get("window").width;
@ -18,14 +20,6 @@ const TEST_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthum
// This will be used in RawPostJsx // This will be used in RawPostJsx
const { SlideInMenu } = renderers; const { SlideInMenu } = renderers;
function pluralize(n, singular, plural) {
if (n < 2) {
return singular;
} else {
return plural;
}
}
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
@ -45,36 +39,6 @@ function getAutoHeight(w1, h1, w2) {
return w2 * (h1 / w1) return w2 * (h1 / w1)
} }
function timeToAge(time1, time2) {
/*
Output a friendly string to describe the age of a post, where `time1` and
`time2` are in milliseconds
*/
const between = (n, lower, upper) => n >= lower && n < upper;
const diff = time1 - time2;
if (diff < 60000) {
return "Seconds ago"
} else if (between(diff, 60000, 3600000)) {
const nMin = Math.floor(diff / 60000);
return nMin + " " + pluralize(nMin, "minute", "minutes") + " ago";
} else if (between(diff, 3600000, 86400000)) {
const nHours = Math.floor(diff / 3600000);
return nHours + " " + pluralize(nHours, "hour", "hours") + " ago";
} else if (between(diff, 86400000, 2629800000)) {
const nDays = Math.floor(diff / 86400000);
return nDays + " " + pluralize(nDays, "day", "days") + " ago";
} else if (between(diff, 2629800000, 31557600000)) {
const nMonths = Math.floor(diff / 2629800000);
return nMonths + " " + pluralize(nMonths, "month", "months") + " ago";
} else {
const nYears = Math.floor(diff / 31557600000);
return nYears + " " + pluralize(nYears, "year", "years") + " ago";
}
}
export const RawPostJsx = (props) => { export const RawPostJsx = (props) => {
const repliesCount = props.data.replies_count; const repliesCount = props.data.replies_count;
@ -125,7 +89,13 @@ export const RawPostJsx = (props) => {
<Text> <Text>
<strong>{ props.data.username }</strong>&nbsp;{ props.data.content } <strong>{ props.data.username }</strong>&nbsp;{ props.data.content }
</Text> </Text>
<TouchableWithoutFeedback> <TouchableWithoutFeedback
onPress = {
() => props.navigation.navigate("ViewComments", {
originTab: props.navigation.getParam("originTab"),
postData: props.data
})
}>
<View> <View>
<Text style = { styles.comments }>{ commentsText }</Text> <Text style = { styles.comments }>{ commentsText }</Text>
</View> </View>
@ -169,7 +139,8 @@ export const PostByDataJsx = (props) => {
<RawPostJsx <RawPostJsx
data = { props.data } data = { props.data }
width = { state.width } width = { state.width }
height = { state.height } /> height = { state.height }
navigation = { props.navigation }/>
: <View></View> } : <View></View> }
</View> </View>
); );
@ -210,7 +181,7 @@ export const PostByIdJsx = (props) => {
}); });
}); });
})(); })();
}); }, []);
return ( return (
<View> <View>
@ -218,7 +189,8 @@ export const PostByIdJsx = (props) => {
<RawPostJsx <RawPostJsx
data = { state } data = { state }
width = { state.width } width = { state.width }
height = { state.height } /> height = { state.height }
navigation = { props.navigation }/>
: <View></View> : <View></View>
} }
</View> </View>

View File

@ -9,7 +9,9 @@ const TimelineViewJsx = (props) => {
{ props.posts.map((post, i) => { { props.posts.map((post, i) => {
return ( return (
<View key = { i } > <View key = { i } >
<PostByDataJsx data = { post } /> <PostByDataJsx
navigation = { props.navigation }
data = { post } />
</View> </View>
); );
}) } }) }
@ -17,4 +19,4 @@ const TimelineViewJsx = (props) => {
); );
}; };
export default TimelineViewJsx; export default TimelineViewJsx;

View File

@ -0,0 +1,37 @@
export function pluralize(n, singular, plural) {
if (n < 2) {
return singular;
} else {
return plural;
}
}
export function timeToAge(time1, time2) {
/*
Output a friendly string to describe the age of a post, where `time1` and
`time2` are in milliseconds
*/
const between = (n, lower, upper) => n >= lower && n < upper;
const diff = time1 - time2;
if (diff < 60000) {
return "Seconds ago"
} else if (between(diff, 60000, 3600000)) {
const nMin = Math.floor(diff / 60000);
return nMin + " " + pluralize(nMin, "minute", "minutes") + " ago";
} else if (between(diff, 3600000, 86400000)) {
const nHours = Math.floor(diff / 3600000);
return nHours + " " + pluralize(nHours, "hour", "hours") + " ago";
} else if (between(diff, 86400000, 2629800000)) {
const nDays = Math.floor(diff / 86400000);
return nDays + " " + pluralize(nDays, "day", "days") + " ago";
} else if (between(diff, 2629800000, 31557600000)) {
const nMonths = Math.floor(diff / 2629800000);
return nMonths + " " + pluralize(nMonths, "month", "months") + " ago";
} else {
const nYears = Math.floor(diff / 31557600000);
return nYears + " " + pluralize(nYears, "year", "years") + " ago";
}
}