diff --git a/.gitignore b/.gitignore index 0ba4ecc..0a98f3d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ web-build/ # macOS .DS_Store + +# Vim +*.sw[klmnop] diff --git a/assets/eva-icons/ellipsis.png b/assets/eva-icons/ellipsis.png new file mode 100644 index 0000000..7bce4cf Binary files /dev/null and b/assets/eva-icons/ellipsis.png differ diff --git a/assets/eva-icons/paper-plane.png b/assets/eva-icons/paper-plane.png new file mode 100644 index 0000000..1432e16 Binary files /dev/null and b/assets/eva-icons/paper-plane.png differ diff --git a/assets/eva-icons/post-actions/bookmark-active.png b/assets/eva-icons/post-actions/bookmark-active.png new file mode 100644 index 0000000..123d321 Binary files /dev/null and b/assets/eva-icons/post-actions/bookmark-active.png differ diff --git a/assets/eva-icons/post-actions/bookmark-inactive.png b/assets/eva-icons/post-actions/bookmark-inactive.png new file mode 100644 index 0000000..142f644 Binary files /dev/null and b/assets/eva-icons/post-actions/bookmark-inactive.png differ diff --git a/assets/eva-icons/post-actions/comment-active.png b/assets/eva-icons/post-actions/comment-active.png deleted file mode 100644 index c9a5a10..0000000 Binary files a/assets/eva-icons/post-actions/comment-active.png and /dev/null differ diff --git a/assets/eva-icons/post-actions/comment-full.png b/assets/eva-icons/post-actions/comment-full.png deleted file mode 100644 index 00951c8..0000000 Binary files a/assets/eva-icons/post-actions/comment-full.png and /dev/null differ diff --git a/assets/eva-icons/post-actions/comment-inactive.png b/assets/eva-icons/post-actions/comment-inactive.png deleted file mode 100644 index 6a44b47..0000000 Binary files a/assets/eva-icons/post-actions/comment-inactive.png and /dev/null differ diff --git a/assets/eva-icons/post-actions/comment.png b/assets/eva-icons/post-actions/comment.png deleted file mode 100644 index 87eb205..0000000 Binary files a/assets/eva-icons/post-actions/comment.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index df5c627..3739d97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6129,6 +6129,11 @@ "resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.2.1.tgz", "integrity": "sha512-/VbpIEp8tSNNHIvstuA3Swx610whci1Zpc9mqNkqn14DkMbw+ORviln2u0XyHG1kPvvwTNGZY6QpeFwxYaSdbQ==" }, + "react-native-popup-menu": { + "version": "0.15.10", + "resolved": "https://registry.npmjs.org/react-native-popup-menu/-/react-native-popup-menu-0.15.10.tgz", + "integrity": "sha512-w7MaicsfpclK7g/omjMchNaXwhMi0apt/DC734AbHuJTWCfv5mF3JgL1UzRW19ncFMBRfQeYapPy/zUyJCGgEQ==" + }, "react-native-reanimated": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-1.9.0.tgz", diff --git a/package.json b/package.json index b48ad8a..bb34cfb 100644 --- a/package.json +++ b/package.json @@ -9,21 +9,22 @@ }, "dependencies": { "@react-native-community/masked-view": "0.1.10", + "@react-navigation/core": "5.2.3", "@react-navigation/native": "5.1.1", + "@react-navigation/stack": "5.2.3", "expo": "^38.0.9", "expo-status-bar": "^1.0.2", "react": "~16.11.0", "react-dom": "~16.11.0", "react-native": "https://github.com/expo/react-native/archive/sdk-38.0.2.tar.gz", "react-native-gesture-handler": "~1.6.0", + "react-native-popup-menu": "^0.15.10", "react-native-reanimated": "~1.9.0", "react-native-safe-area-context": "~3.0.7", "react-native-screens": "~2.9.0", "react-native-web": "~0.11.7", "react-navigation": "^4.4.0", - "react-navigation-stack": "^2.8.2", - "@react-navigation/stack": "5.2.3", - "@react-navigation/core": "5.2.3" + "react-navigation-stack": "^2.8.2" }, "devDependencies": { "@babel/core": "^7.8.6", diff --git a/src/App.js b/src/App.js index 5a25377..9b7d6db 100644 --- a/src/App.js +++ b/src/App.js @@ -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: { diff --git a/src/components/navigation/navigators.js b/src/components/navigation/navigators.js index b7ba237..d3776c1 100644 --- a/src/components/navigation/navigators.js +++ b/src/components/navigation/navigators.js @@ -1,43 +1,69 @@ import React from "react"; import { View } from "react-native"; import { ScrollView } from "react-native-gesture-handler"; + +import { MenuProvider } from "react-native-popup-menu"; + import BackBarJsx from "./back-bar"; import TrayJsx from "src/components/navigation/tray"; +// Provider for context menus +// Allows for establishing global styling of context menus +export const ContextJsx = (props) => { + return ( + + { props.children } + + ); +}; + export const ScreenWithTrayJsx = (props) => { return ( - - - { props.children } + + + + { props.children } - - ) + active = { props.active } + navigation = { props.navigation } /> + + + ); }; export const ScreenWithBackBarJsx = (props) => { return ( - - - - { props.children } - - + + + + + { props.children } + + + ); }; export const ScreenWithFullNavigationJsx = (props) => { return ( - - - - { props.children } - - - + + + + + { props.children } + + + + ); -} \ No newline at end of file +}; + +const providerStyles = { + backdrop: { + backgroundColor: "black", + opacity: 0.5 + } +} diff --git a/src/components/pages/feed.js b/src/components/pages/feed.js index c742724..e507703 100644 --- a/src/components/pages/feed.js +++ b/src/components/pages/feed.js @@ -12,12 +12,13 @@ const TEST_POSTS = [ id: 1, avatar: TEST_IMAGE, username: "njms", + replies_count: 3, favourited: false, reblogged: false, content: "Also learning Claire de Lune feels a lot like reading the communist manifesto", timestamp: 1596745156000, media_attachments: [ - {preview_url: TEST_IMAGE} + {url: TEST_IMAGE} ] }, { @@ -26,10 +27,13 @@ const TEST_POSTS = [ username: "njms", favourited: false, reblogged: false, + replies_count: 0, content: "Also learning Claire de Lune feels a lot like reading the communist manifesto", timestamp: 1596745156000, media_attachments: [ - {preview_url: TEST_IMAGE} + { url: "https://college.mayo.edu/media/mccms/content-assets/campus-amp-community/arizona/mayo-clinic-phoenix-arizona-is453080663-hero-mobile.jpg" }, + { url: TEST_IMAGE }, + { url: TEST_IMAGE } ] } ]; @@ -41,13 +45,17 @@ const FeedJsx = (props) => { - + + + - + You're all caught up. @@ -60,7 +68,7 @@ const FeedJsx = (props) => { - + ); }; @@ -97,4 +105,4 @@ const styles = { } }; -export default FeedJsx; \ No newline at end of file +export default FeedJsx; diff --git a/src/components/pages/view-comments.js b/src/components/pages/view-comments.js new file mode 100644 index 0000000..a385d5f --- /dev/null +++ b/src/components/pages/view-comments.js @@ -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 ( + + + + + { props.data.username }  + { props.data.content } + + + + + { timeToAge((new Date()).getTime(), props.data.created_at) } + + + + + + Reply + + + + + + + + + + ); +} + +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 ( + + + + + { state.loaded ? + + + + + + { + state.descendants.map((thread, i) => { + const comment = thread[0]; + const subs = thread.slice(1); + + return ( + + + { + subs.map((sub, j) => { + return ( + + + + ) + }) + } + + ); + }) + } + + + : + } + + + + setState({...state, reply: c }) }/> + + + + + + + + + ); +} + +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; diff --git a/src/components/pages/view-post.js b/src/components/pages/view-post.js index 106fcd7..5cc87ce 100644 --- a/src/components/pages/view-post.js +++ b/src/components/pages/view-post.js @@ -8,9 +8,11 @@ const ViewPostJsx = (props) => { - + ); } -export default ViewPostJsx; \ No newline at end of file +export default ViewPostJsx; diff --git a/src/components/posts/paged-grid.js b/src/components/posts/paged-grid.js index 25a0542..50e964c 100644 --- a/src/components/posts/paged-grid.js +++ b/src/components/posts/paged-grid.js @@ -89,4 +89,4 @@ const styles = { } } -export default PagedGridJSX; \ No newline at end of file +export default PagedGridJSX; diff --git a/src/components/posts/post-action-bar.js b/src/components/posts/post-action-bar.js index 543c7c0..458cc9d 100644 --- a/src/components/posts/post-action-bar.js +++ b/src/components/posts/post-action-bar.js @@ -27,10 +27,8 @@ function reblogCallback(state, updater) { invertField("reblogged", state, updater); } -function downloadCallback(state, updater) { - let newState = state; - newState.downloaded = true; - updater(newState); +function bookmarkCallback(state, updater) { + invertField("bookmarked", state, updater); } const PostActionJsx = (props) => { @@ -41,7 +39,12 @@ const PostActionJsx = (props) => { source = { activeOrNot(props.state[props.field], props.pack) } - style = { styles.icon } /> + style = { + [ + styles.icon, + props.last ? styles.lastIcon : {} + ] + } /> ) } @@ -51,7 +54,7 @@ const PostActionBarJsx = (props) => { favourited: props.favourited, commenting: false, reblogged: props.reblogged, - downloaded: false + bookmarked: false }); const icons = { @@ -59,17 +62,13 @@ const PostActionBarJsx = (props) => { active: require("assets/eva-icons/post-actions/heart-active.png"), inactive: require("assets/eva-icons/post-actions/heart-inactive.png") }, - comment: { - active: require("assets/eva-icons/post-actions/comment-active.png"), - inactive: require("assets/eva-icons/post-actions/comment-inactive.png") - }, reblog: { active: require("assets/eva-icons/post-actions/reblog-active.png"), inactive: require("assets/eva-icons/post-actions/reblog-inactive.png") }, - download: { - active: require("assets/eva-icons/post-actions/download-active.png"), - inactive: require("assets/eva-icons/post-actions/download-inactive.png") + bookmark: { + active: require("assets/eva-icons/post-actions/bookmark-active.png"), + inactive: require("assets/eva-icons/post-actions/bookmark-inactive.png") } } return ( @@ -79,13 +78,7 @@ const PostActionBarJsx = (props) => { pack = { icons.heart } state = { state } callback = { () => favouritedCallback(state, setState) } /> - - commentCallback(state, setState) } /> - + { callback = { () => reblogCallback(state, setState) } /> downloadCallback(state, setState) } /> + callback = { () => bookmarkCallback(state, setState) } /> ) } +const SCREEN_WIDTH = Dimensions.get("window").width; const styles = { flexContainer: { display: "flex", flexDirection: "row", - padding: Dimensions.get("window").width / 40 + padding: SCREEN_WIDTH / 40 }, icon: { width: 30, height: 30, - marginRight: Dimensions.get("window").width / 20 + marginRight: SCREEN_WIDTH / 20 + }, + lastIcon: { + marginLeft: "auto" } } -export default PostActionBarJsx; \ No newline at end of file +export default PostActionBarJsx; diff --git a/src/components/posts/post.js b/src/components/posts/post.js index cd6dde5..b8fc264 100644 --- a/src/components/posts/post.js +++ b/src/components/posts/post.js @@ -1,11 +1,33 @@ import React, { useEffect, useState } from "react"; -import { Image, View, Text, Dimensions } from "react-native"; +import { + Image, + View, + Text, + Dimensions, + TouchableWithoutFeedback, + ScrollView +} from "react-native"; + +import { + Menu, + MenuOptions, + MenuOption, + MenuTrigger, + 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; 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 @@ -25,62 +47,107 @@ 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 - */ +function getDimensionsPromises(uris) { + return uris.map(attachment => new Promise(resolve => { + Image.getSize(attachment.url, (width, height) => { + const autoHeight = getAutoHeight(width, height, SCREEN_WIDTH) - const between = (n, lower, upper) => n >= lower && n < upper; - const pluralize = (n, singular, plural) => n < 2 ? singular : plural; - - 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"; - } + resolve([SCREEN_WIDTH, autoHeight]); + }); + })); } +const PostImageJsx = (props) => { + return +}; + export const RawPostJsx = (props) => { + const repliesCount = props.data.replies_count; + + let commentsText; + if (repliesCount == 0) { + commentsText = "View comments"; + } else { + commentsText = "View " + + repliesCount + + pluralize(repliesCount, " comment", " comments"); + } + return ( - - { props.data.username } + + + + + + + + + + + + - { /* TODO: support for more than one image per post */ } - - 1 ? + + { + props.data.media_attachments + .map((attachment, i) => { + return (); + }) + } + + : + } + + reblogged = { props.data.reblogged } /> { props.data.username } { props.data.content } + props.navigation.navigate("ViewComments", { + originTab: props.navigation.getParam("originTab"), + postData: props.data + }) + }> + + { commentsText } + + + { timeToAge((new Date()).getTime(), props.data.timestamp) } @@ -96,19 +163,16 @@ export const PostByDataJsx = (props) => { */ let [state, setState] = useState({ - width: 0, - height: 0, - loaded: false + loaded: false, + dimensions: [] }); useEffect(() => { - Image.getSize(TEST_IMAGE, (width, height) => { - const newHeight = getAutoHeight(width, height, SCREEN_WIDTH) - + Promise.all(getDimensionsPromises(props.data.media_attachments)) + .then(dimensions => { setState({ - width: SCREEN_WIDTH, - height: newHeight, - loaded: true + dimensions: dimensions, + loaded: true }); }); }); @@ -118,8 +182,8 @@ export const PostByDataJsx = (props) => { { state.loaded ? + dimensions = { state.dimensions } + navigation = { props.navigation }/> : } ); @@ -138,25 +202,25 @@ export const PostByIdJsx = (props) => { reblogged: false, content: "", timestamp: 0, + loaded: false, + dimensions: [] }); useEffect(() => { // TODO: Make API request using props.id, set it as the state - ((/* This would be the data retrieved */) => { - Image.getSize(TEST_IMAGE, (width, height) => { - const newHeight = getAutoHeight(width, height, SCREEN_WIDTH) - + (() => { + Promise.all(getDimensionsPromises([{ url: TEST_IMAGE }])) + .then(dimensions => { setState({ avatar: TEST_IMAGE, username: "njms", - media_attachments: [TEST_IMAGE], + media_attachments: [{ url: TEST_IMAGE }], favourited: false, reblogged: false, content: "Also learning Claire de Lune feels a lot like reading the communist manifesto", timestamp: 1596745156000, - width: SCREEN_WIDTH, - height: newHeight, - loaded: true + loaded: true, + dimensions: dimensions }); }); })(); @@ -164,11 +228,11 @@ export const PostByIdJsx = (props) => { return ( - { state.loaded ? - + dimensions = { state.dimensions } + navigation = { props.navigation }/> : } @@ -191,19 +255,38 @@ const styles = { color: "#000", marginTop: -2 }, + menu: { + marginLeft: "auto", + marginRight: SCREEN_WIDTH / 30 + }, pfp: { width: SCREEN_WIDTH / 10, height: SCREEN_WIDTH / 10, marginRight: SCREEN_WIDTH / 28, borderRadius: 50 }, + ellipsis: { + width: SCREEN_WIDTH / 15, + height: SCREEN_WIDTH / 15 + }, photo: { flex: 1, }, - + carousel: { + width: SCREEN_WIDTH, + height: SCREEN_WIDTH, + }, + carouselContainer: { + display: "flex", + alignItems: "center" + }, caption: { padding: SCREEN_WIDTH / 24, }, + comments: { + paddingTop: SCREEN_WIDTH / 50, + color: "#666", + }, captionDate: { fontSize: 0.8, color: "#666", @@ -211,6 +294,28 @@ const styles = { }, strong: { fontWeight: 'bold', - color: "#666", } -}; \ No newline at end of file +}; + +// customStyles for react-native-popup-menu should be defined in particular +// objects to be interpreted correctly. + +//const menuStyles = { +// menuProviderWrapper +//} + +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 + } +} diff --git a/src/components/posts/timeline-view.js b/src/components/posts/timeline-view.js index a66736b..70a158f 100644 --- a/src/components/posts/timeline-view.js +++ b/src/components/posts/timeline-view.js @@ -9,7 +9,9 @@ const TimelineViewJsx = (props) => { { props.posts.map((post, i) => { return ( - + ); }) } @@ -17,4 +19,4 @@ const TimelineViewJsx = (props) => { ); }; -export default TimelineViewJsx; \ No newline at end of file +export default TimelineViewJsx; diff --git a/src/interface/rendering.js b/src/interface/rendering.js new file mode 100644 index 0000000..6dee69e --- /dev/null +++ b/src/interface/rendering.js @@ -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"; + } +}