|
@ -12,3 +12,6 @@ web-build/
|
|||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Vim
|
||||
*.sw[klmnop]
|
||||
|
|
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 986 B |
Before Width: | Height: | Size: 814 B |
Before Width: | Height: | Size: 2.7 KiB |
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 }
|
||||
}, {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -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,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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,15 +90,45 @@ 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 } />
|
||||
|
@ -81,6 +136,18 @@ export const RawPostJsx = (props) => {
|
|||
<Text>
|
||||
<Text style = { styles.strong }>{ props.data.username }</Text> { 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}) }
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|