Merge pull request #8 from natjms/posts

Implementing posts
This commit is contained in:
Nat 2021-03-13 13:29:28 -04:00 committed by GitHub
commit f0b7fb2763
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 683 additions and 132 deletions

3
.gitignore vendored
View File

@ -12,3 +12,6 @@ web-build/
# macOS
.DS_Store
# Vim
*.sw[klmnop]

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

5
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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,6 +23,7 @@ const Stack = createStackNavigator({
Profile: { screen: ProfileJsx, },
Search: { screen: SearchJsx },
ViewPost: { screen: ViewPostJsx },
ViewComments: { screen: ViewCommentsJsx },
ViewProfile: { screen: ViewProfileJsx },
ViewHashtag: { screen: ViewHashtagJsx }
}, {

View File

@ -1,11 +1,25 @@
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 (
<MenuProvider customStyles = { providerStyles }>
{ props.children }
</MenuProvider>
);
};
export const ScreenWithTrayJsx = (props) => {
return (
<ContextJsx>
<View style = { { flex: 1 } }>
<ScrollView>
{ props.children }
@ -14,22 +28,26 @@ export const ScreenWithTrayJsx = (props) => {
active = { props.active }
navigation = { props.navigation } />
</View>
)
</ContextJsx>
);
};
export const ScreenWithBackBarJsx = (props) => {
return (
<ContextJsx>
<View style = { { flex: 1 } }>
<BackBarJsx navigation = { props.navigation } />
<ScrollView>
{ props.children }
</ScrollView>
</View>
</ContextJsx>
);
};
export const ScreenWithFullNavigationJsx = (props) => {
return (
<ContextJsx>
<View style = { { flex: 1 } }>
<BackBarJsx navigation = { props.navigation } />
<ScrollView>
@ -39,5 +57,13 @@ export const ScreenWithFullNavigationJsx = (props) => {
active = { props.active }
navigation = { props.navigation } />
</View>
</ContextJsx>
);
};
const providerStyles = {
backdrop: {
backgroundColor: "black",
opacity: 0.5
}
}

View File

@ -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,8 +45,12 @@ const FeedJsx = (props) => {
<ScreenWithTrayJsx
active = "Feed"
navigation = { props.navigation }>
<TimelineViewJsx posts = { TEST_POSTS } />
<TimelineViewJsx
navigation = { props.navigation }
posts = { TEST_POSTS } />
<View style = { styles.interruptionOuter }>
<View style = { styles.interruption }>
<Image
source = { checkmark }

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,7 +8,9 @@ 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>
);
}

View File

@ -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 : {}
]
} />
</TouchableWithoutFeedback>
)
}
@ -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 (
@ -80,12 +79,6 @@ const PostActionBarJsx = (props) => {
state = { state }
callback = { () => favouritedCallback(state, setState) } />
<PostActionJsx
field = "commenting"
pack = { icons.comment }
state = { state }
callback = { () => commentCallback(state, setState) } />
<PostActionJsx
field = "reblogged"
pack = { icons.reblog }
@ -93,24 +86,29 @@ const PostActionBarJsx = (props) => {
callback = { () => reblogCallback(state, setState) } />
<PostActionJsx
field = "downloaded"
pack = { icons.download }
field = "bookmarked"
pack = { icons.bookmark }
last = { true }
state = { state }
callback = { () => downloadCallback(state, setState) } />
callback = { () => bookmarkCallback(state, setState) } />
</View>
)
}
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"
}
}

View File

@ -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,38 +47,41 @@ 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 <Image
source = { { uri: props.uri } }
style = {
{
flex: 1,
width: SCREEN_WIDTH,
height: getAutoHeight(props.width, props.height, SCREEN_WIDTH),
objectFit: "cover"
}
} />
};
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 (
<View>
<View style = { styles.postHeader }>
@ -65,22 +90,64 @@ export const RawPostJsx = (props) => {
source = { { uri: props.data.avatar } } />
<Text
style = { styles.postHeaderName }>{ props.data.username }</Text>
</View>
{ /* TODO: support for more than one image per post */ }
<View style = { styles.menu }>
<Menu renderer = { SlideInMenu }>
<MenuTrigger>
<Image
source = { { uri: TEST_IMAGE/* props.data.media_attachments[0] */ } }
style = { {
flex: 1,
width: SCREEN_WIDTH,
height: getAutoHeight(props.width, props.height, SCREEN_WIDTH)
} } />
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>
{
props.data.media_attachments.length > 1 ?
<ScrollView
horizontal = { true }
snapToInterval = { SCREEN_WIDTH }
decelerationRate = { "fast" }
style = { styles.carousel }
contentContainerStyle = { styles.carouselContainer }>
{
props.data.media_attachments
.map((attachment, i) => {
return (<PostImageJsx
key = { i }
uri = { attachment.url }
width = { props.dimensions[i][0] }
height = { props.dimensions[i][1] } />);
})
}
</ScrollView>
: <PostImageJsx
uri = { props.data.media_attachments[0].url }
width = { props.dimensions[0][0] }
height = { props.dimensions[0][1] } />
}
<PostActionBarJsx
favourited = { props.data.favourited }
reblogged = {props.data.reblogged } />
reblogged = { props.data.reblogged } />
<View style = { styles.caption }>
<Text>
<Text style = { styles.strong }>{ props.data.username }</Text>&nbsp;{ props.data.content }
</Text>
<TouchableWithoutFeedback
onPress = {
() => props.navigation.navigate("ViewComments", {
originTab: props.navigation.getParam("originTab"),
postData: props.data
})
}>
<View>
<Text style = { styles.comments }>{ commentsText }</Text>
</View>
</TouchableWithoutFeedback>
<Text style = { styles.captionDate }>
{ timeToAge((new Date()).getTime(), props.data.timestamp) }
</Text>
@ -96,18 +163,15 @@ 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,
dimensions: dimensions,
loaded: true
});
});
@ -118,8 +182,8 @@ export const PostByDataJsx = (props) => {
{ state.loaded ?
<RawPostJsx
data = { props.data }
width = { state.width }
height = { state.height } />
dimensions = { state.dimensions }
navigation = { props.navigation }/>
: <View></View> }
</View>
);
@ -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
});
});
})();
@ -167,8 +231,8 @@ export const PostByIdJsx = (props) => {
{ state.loaded ?
<RawPostJsx
data = { state }
width = { state.width }
height = { state.height } />
dimensions = { state.dimensions }
navigation = { props.navigation }/>
: <View></View>
}
</View>
@ -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",
}
};
// 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
}
}

View File

@ -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>
);
}) }

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";
}
}