Create ViewComments page to display post context
This commit is contained in:
parent
09424ac4eb
commit
4d458734ca
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
|
@ -6,9 +6,11 @@ import { createStackNavigator } from "react-navigation-stack";
|
|||
|
||||
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 ProfileJsx, { ViewProfileJsx } from "src/components/pages/profile";
|
||||
import ViewPostJsx from "src/components/pages/view-post";
|
||||
import DiscoverJsx from 'src/components/pages/discover';
|
||||
import SearchJsx from 'src/components/pages/discover/search';
|
||||
import ViewHashtagJsx from 'src/components/pages/discover/view-hashtag';
|
||||
|
@ -21,9 +23,10 @@ const Stack = createStackNavigator({
|
|||
Profile: { screen: ProfileJsx, },
|
||||
Search: { screen: SearchJsx },
|
||||
ViewPost: { screen: ViewPostJsx },
|
||||
ViewComments: { screen: ViewCommentsJsx },
|
||||
ViewProfile: { screen: ViewProfileJsx },
|
||||
ViewHashtag: { screen: ViewHashtagJsx }
|
||||
}, {
|
||||
}, {
|
||||
initialRouteKey: "Feed",
|
||||
headerMode: "none",
|
||||
navigationOptions: {
|
||||
|
|
|
@ -9,7 +9,7 @@ import TrayJsx from "src/components/navigation/tray";
|
|||
|
||||
// Provider for context menus
|
||||
// Allows for establishing global styling of context menus
|
||||
const ContextJsx = (props) => {
|
||||
export const ContextJsx = (props) => {
|
||||
return (
|
||||
<MenuProvider customStyles = { providerStyles }>
|
||||
{ props.children }
|
||||
|
|
|
@ -43,7 +43,9 @@ const FeedJsx = (props) => {
|
|||
<ScreenWithTrayJsx
|
||||
active = "Feed"
|
||||
navigation = { props.navigation }>
|
||||
<TimelineViewJsx posts = { TEST_POSTS } />
|
||||
<TimelineViewJsx
|
||||
navigation = { props.navigation }
|
||||
posts = { TEST_POSTS } />
|
||||
<div style = { styles.interruptionOuter }>
|
||||
<View style = { styles.interruption }>
|
||||
<Image
|
||||
|
|
|
@ -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>
|
||||
{ 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;
|
|
@ -8,9 +8,11 @@ const ViewPostJsx = (props) => {
|
|||
<ScreenWithFullNavigationJsx
|
||||
active = { props.navigation.getParam("originTab", "Timeline") }
|
||||
navigation = { props.navigation }>
|
||||
<PostByIdJsx id = { props.id } />
|
||||
<PostByIdJsx
|
||||
navigation = { props.navigation }
|
||||
id = { props.id } />
|
||||
</ScreenWithFullNavigationJsx>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewPostJsx;
|
||||
export default ViewPostJsx;
|
||||
|
|
|
@ -89,4 +89,4 @@ const styles = {
|
|||
}
|
||||
}
|
||||
|
||||
export default PagedGridJSX;
|
||||
export default PagedGridJSX;
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
renderers
|
||||
} from "react-native-popup-menu";
|
||||
|
||||
import { pluralize, timeToAge } from "src/interface/rendering"
|
||||
|
||||
import PostActionBarJsx from "src/components/posts/post-action-bar";
|
||||
|
||||
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
|
||||
const { SlideInMenu } = renderers;
|
||||
|
||||
function pluralize(n, singular, plural) {
|
||||
if (n < 2) {
|
||||
return singular;
|
||||
} else {
|
||||
return plural;
|
||||
}
|
||||
}
|
||||
|
||||
function getAutoHeight(w1, h1, w2) {
|
||||
/*
|
||||
Given the original dimensions and the new width, calculate what would
|
||||
|
@ -45,36 +39,6 @@ function getAutoHeight(w1, h1, w2) {
|
|||
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) => {
|
||||
const repliesCount = props.data.replies_count;
|
||||
|
||||
|
@ -125,7 +89,13 @@ export const RawPostJsx = (props) => {
|
|||
<Text>
|
||||
<strong>{ props.data.username }</strong> { props.data.content }
|
||||
</Text>
|
||||
<TouchableWithoutFeedback>
|
||||
<TouchableWithoutFeedback
|
||||
onPress = {
|
||||
() => props.navigation.navigate("ViewComments", {
|
||||
originTab: props.navigation.getParam("originTab"),
|
||||
postData: props.data
|
||||
})
|
||||
}>
|
||||
<View>
|
||||
<Text style = { styles.comments }>{ commentsText }</Text>
|
||||
</View>
|
||||
|
@ -169,7 +139,8 @@ export const PostByDataJsx = (props) => {
|
|||
<RawPostJsx
|
||||
data = { props.data }
|
||||
width = { state.width }
|
||||
height = { state.height } />
|
||||
height = { state.height }
|
||||
navigation = { props.navigation }/>
|
||||
: <View></View> }
|
||||
</View>
|
||||
);
|
||||
|
@ -210,7 +181,7 @@ export const PostByIdJsx = (props) => {
|
|||
});
|
||||
});
|
||||
})();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View>
|
||||
|
@ -218,7 +189,8 @@ export const PostByIdJsx = (props) => {
|
|||
<RawPostJsx
|
||||
data = { state }
|
||||
width = { state.width }
|
||||
height = { state.height } />
|
||||
height = { state.height }
|
||||
navigation = { props.navigation }/>
|
||||
: <View></View>
|
||||
}
|
||||
</View>
|
||||
|
|
|
@ -9,7 +9,9 @@ const TimelineViewJsx = (props) => {
|
|||
{ props.posts.map((post, i) => {
|
||||
return (
|
||||
<View key = { i } >
|
||||
<PostByDataJsx data = { post } />
|
||||
<PostByDataJsx
|
||||
navigation = { props.navigation }
|
||||
data = { post } />
|
||||
</View>
|
||||
);
|
||||
}) }
|
||||
|
@ -17,4 +19,4 @@ const TimelineViewJsx = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default TimelineViewJsx;
|
||||
export default TimelineViewJsx;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue