Compare commits

..

10 Commits

Author SHA1 Message Date
Adolfo Reyna
fe9fc8e3e4 Display selected tag name in the Tags screen header 2025-02-28 00:35:22 -05:00
Adolfo Reyna
8cdcfefa0d Add tag functionality to posts and implement Tags screen 2025-02-28 00:21:00 -05:00
Adolfo Reyna
f5c7ff38dd Add logic to handle notifications to open profile or posts 2025-02-27 23:04:56 -05:00
Adolfo Reyna
733c1fd793 Share post text to be able to copy 2025-02-22 00:20:51 -05:00
Adolfo Reyna
91e9740159 Add photo to comments 2025-02-22 00:07:35 -05:00
Adolfo Reyna
a1b2143337 Provide access to change the API base url 2025-02-22 00:00:59 -05:00
Adolfo Reyna
ad394a6f18 Open comments on singlepost view 2025-02-22 00:00:29 -05:00
Adolfo Reyna
0ae13c9ffe Enable profile photo touch 2025-02-22 00:00:07 -05:00
Adolfo Reyna
13406f2774 Show version on menu 2025-02-10 22:05:51 -05:00
Adolfo Reyna
8f62e4b953 Point to new server for test 2025-02-10 21:44:54 -05:00
11 changed files with 254 additions and 64 deletions

18
API.js
View File

@@ -1,4 +1,5 @@
const baseUrl = "https://api.emmint.com";
const baseUrl = "https://emiapi.reynafamily.com";
//const baseUrl = "http://localhost:3000";
let getCall = async (path = "", params = {}) => {
@@ -6,7 +7,8 @@ let getCall = async (path = "", params = {}) => {
Object.keys(params).forEach(p => {
queryParams += p + "=" + params[p] + "&"
});
return fetch(baseUrl + path + queryParams, {
let localBaseUrl = global.baseUrl ?? baseUrl;
return fetch(localBaseUrl + path + queryParams, {
method: 'GET',
mode: 'cors',
credentials: 'include',
@@ -15,6 +17,7 @@ let getCall = async (path = "", params = {}) => {
}
}).then(response => response.json()).catch((error) => {
console.error(error);
console.trace();
})
}
@@ -23,7 +26,8 @@ let deleteCall = async (path = "", params = {}) => {
Object.keys(params).forEach(p => {
queryParams += p + "=" + params[p] + "&"
});
return fetch(baseUrl + path + queryParams, {
let localBaseUrl = global.baseUrl ?? baseUrl;
return fetch(localBaseUrl + path + queryParams, {
method: 'DELETE',
mode: 'cors',
credentials: 'include',
@@ -32,11 +36,13 @@ let deleteCall = async (path = "", params = {}) => {
}
}).then(response => response.json()).catch((error) => {
console.error(error);
console.trace();
})
}
let postCall = async (path, params) => {
return fetch(baseUrl + path, {
let localBaseUrl = global.baseUrl ?? baseUrl;
return fetch(localBaseUrl + path, {
method: 'POST',
mode: 'cors',
credentials: 'include',
@@ -46,6 +52,7 @@ let postCall = async (path, params) => {
}
}).then(response => response.json()).catch((error) => {
console.error(error);
console.trace();
})
}
@@ -136,6 +143,9 @@ const API = {
if (userid) return getCall("/post/usr/" + userid);
return getCall("/post/");
},
getPostsByTag(tag) {
return getCall("/post/tag/" + tag);
},
getPostsWithTag(userid, tag = "images") {
if (userid) return getCall("/post/usr/" + userid + "/" + tag);
return getCall("/post/" + tag);

26
App.js
View File

@@ -8,6 +8,7 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import Login from "./Views/Login.js"
import Feed from "./Views/Feed.js"
import Profile from "./Views/Profile.js"
import Tags from "./Views/Tags.js"
import Search from './Views/Search.js';
import Groups from './Views/Groups.js';
import Courses from './Views/Courses.js';
@@ -31,6 +32,7 @@ import SongPlayer from './Views/SongPlayer.js';
import { Platform } from 'react-native';
import { PostHogProvider } from 'posthog-react-native'
import * as Updates from 'expo-updates';
import { useNavigation } from '@react-navigation/native';
const Tab = createBottomTabNavigator();
@@ -95,6 +97,7 @@ const MainNavigation = ({ route }) => {
const [notification, setNotification] = useState(false);
const notificationListener = useRef();
const responseListener = useRef();
const mainNavigation = useNavigation();
useEffect(() => {
registerForPushNotificationsAsync().then(async (token) => {
let isLoggedIn = await API.isLoggedIn();
@@ -105,14 +108,27 @@ const MainNavigation = ({ route }) => {
// This listener is fired whenever a notification is received while the app is foregrounded
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
//console.log("got notif", notification)
//console.log("got notif", notification);
setNotification(notification);
});
// This listener is fired whenever a user taps on or interacts with a notification (works when app is foregrounded, backgrounded, or killed)
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
//console.log("got notif click", notification)
console.log(response);
const data = response.notification.request.content.data;
if (data && Object.keys(data).length > 0) {
try {
if (data.profile_id) {
mainNavigation.navigate("Profile", { profileid: data.profile_id });
}
if (data.post_id) {
mainNavigation.navigate("SinglePost", { postid: data.post_id });
}
} catch (error) {
alert("Error: " + error);
}
} else {
//alert("Notification clicked but no data found.");
}
});
const interval = setInterval(async () => {
@@ -291,6 +307,10 @@ export default function App() {
name="Profile"
component={Profile}
/>
<Stack.Screen
name="Tags"
component={Tags}
/>
<Stack.Screen
name="NewPost"
component={NewPostView}

View File

@@ -10,6 +10,7 @@ import { useSnapshot } from 'valtio';
import GlobalState from '../contexts/GlobalState.js';
import ProfileCardHorizontal from "../components/ProfileCardHorizontal.js";
import { reloadAppAsync } from "expo";
import * as Updates from 'expo-updates';
let MenuView = ({ navigation }) => {
@@ -93,6 +94,10 @@ let MenuView = ({ navigation }) => {
<RadioButton.Item value="fr" label="French" />
</RadioButton.Group>
</View>
<View style={{ padding: 10, alignContent: "center", flex: 1 }}>
<Text>Version: {Updates.runtimeVersion}</Text>
<Text>Channel: {Updates.Channel}</Text>
</View>
</ImageBackground>
</ScrollView>
)

View File

@@ -21,7 +21,7 @@ let NotificationsView = ({ navigation, route }) => {
<Text style={{ fontWeight: 'normal', fontSize: 12 }}>
{" " + Moment(item.ts).fromNow()}
</Text>
<SinglePost postId={item.postid} />
<SinglePost postId={item.postid} hideComments={true} />
</Card.Content>
</Card>
)

75
Views/Tags.js Normal file
View File

@@ -0,0 +1,75 @@
import { StatusBar } from 'expo-status-bar';
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, SafeAreaView, FlatList } from 'react-native';
import API from './../API.js';
import Post from './../components/Post.js';
let Tags = ({ navigation, route }) => {
let [Posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let subscribed = true;
const getData = async () => {
setPosts([]);
console.log("Posts by tag", route.params.tag);
API.getPostsByTag(route.params.tag).then((data) => {
if(!subscribed) return 0;
console.log("Posts by tag", data);
setPosts(data);
setLoading(false);
});
}
getData();
return ()=>{
subscribed = false;
}
}, [route.params?.tag]);
const renderPost = (({ item }) => {
if (item.nonOrganicType)
return (<></>);
return (<Post post={item} />);
});
const header = (
<View>
<Text style={{ fontSize: 20, fontWeight: 'bold', padding: 10, alignContent: 'center', textAlign: 'center' }}>
#{route.params.tag}
</Text>
</View>
)
return (
<SafeAreaView style={styles.container}>
<View>
{!loading ?
<FlatList
data={Posts}
renderItem={renderPost}
keyExtractor={item => item.lastUpdated || item._id || item.ceatedAt}
ListHeaderComponent={header}
refreshing={loading}
initialNumToRender={3}
maxToRenderPerBatch={3}
removeClippedSubviews={true}
onRefresh={() => {
API.getPostsByTag(route.params.tag).then(setPosts);
}}
/> :
<></> //TODO: Add empty profile card here
}
</View>
<StatusBar style="auto" />
</SafeAreaView>
);
}
export default Tags;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#edf2f7",
},
});

View File

@@ -10,6 +10,7 @@ import Moment from 'moment';
import i18n from "../i18nMessages.js";
import 'moment/min/locales';
Moment.locale(i18n.locale);
import ProfilePhotoCircle from './ProfilePhotoCircle.js';
let Comment = ({ comment, postid }) => {
const gState = useSnapshot(GlobalState);
@@ -19,7 +20,7 @@ let Comment = ({ comment, postid }) => {
const newCommentReaction = () => {
if (!comment.reactions[viewer._id]) {
comment.reactions[viewer._id] = { type: "like" };
changeLikes(likes+1);
changeLikes(likes + 1);
API.newCommentReaction(postid, comment.createdAt);
} else {
//API.removePostReaction(viewer._id).then(() => {
@@ -31,14 +32,20 @@ let Comment = ({ comment, postid }) => {
return (
<Card style={styles.comment}>
<Card.Content>
<View style={{flexDirection: "row", alignItems: "center", justifyContent: "center"}}>
<View style={{flex:8}}>
<Text style={styles.userName}>
<UserName profileid={comment.profileid} key={comment.profileid} />
<Text style={{fontSize: 12, fontWeight: "normal"}}> {Moment(comment.createdAt).fromNow()}</Text>
</Text>
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "center" }}>
<View style={{ flex: 8, marginBottom: 8 }}>
<ProfilePhotoCircle profileid={comment.profileid} />
<Text style={
{
fontSize: 12,
fontWeight: "normal",
position: "absolute",
top: 20,
left: 37,
}
}> {Moment(comment.createdAt).fromNow()}</Text>
</View>
<View style={{flex:2}}>
<View style={{ flex: 2 }}>
<Button
icon={comment.reactions[viewer._id] ? "favorite" : "favorite-border"}
dense={true}
@@ -46,8 +53,8 @@ let Comment = ({ comment, postid }) => {
>{likes ? likes : ''}</Button>
</View>
</View>
<Text style={{fontSize: 14}}>{cleanContent}</Text>
<Media content={comment.content} postId={postid} skiptVideo={true}/>
<Text style={{ fontSize: 14 }}>{cleanContent}</Text>
<Media content={comment.content} postId={postid} skiptVideo={true} />
</Card.Content>
</Card>
);
@@ -60,12 +67,6 @@ const styles = StyleSheet.create({
margin: 8,
marginTop: 0,
},
userName: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 5,
fontSize: 16
},
likeComment: {
position: 'absolute',
margin: 16,

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Text, ScrollView, FlatList, StyleSheet, View, Share } from 'react-native';
import { Text, Pressable, FlatList, StyleSheet, View, Share, Alert, Linking } from 'react-native';
import Hyperlink from 'react-native-hyperlink'
import { Button, Card, Chip } from 'react-native-paper';
import API from './../API.js';
@@ -13,18 +13,21 @@ import GlobalState from '../contexts/GlobalState.js';
import i18n from "../i18nMessages.js";
import ProfilePhotoCircle from './ProfilePhotoCircle.js';
import { posthog } from './../PostHog.js';
import { useNavigation } from '@react-navigation/native';
import ParsedText from 'react-native-parsed-text';
let Post = (props) => {
const gState = useSnapshot(GlobalState);
const viewer = gState.me;
let [showCommentsB, changeshowCommentsB] = useState(false);
let [showCommentsB, changeshowCommentsB] = useState(props.showComments || false);
let [post, changePost] = useState(props.post);
let [likes, changeLikes] = useState(Object.keys(post.reactions).length);
let [bookmarked, changeBookmarked] = useState(post.bookmarks && post.bookmarks.includes(viewer._id));
let toProfileText = post.toProfile && post.toProfile !== post.profileid ?
<ProfilePhotoCircle profileid={post.toProfile} small={true} /> : undefined;
let cleanContent = post.content.replace(/@[A-z]+:.+\w/g, '').trim();
const navigation = useNavigation();
//cleanContent = convertLinks(cleanContent);
const newComentAdded = (commentData) => {
let newPostObj = { ...post };
@@ -65,6 +68,29 @@ let Post = (props) => {
const renderComment = ({ item }) => (
<Comment comment={item} postid={post._id} />
);
const handleTagPress = (tag) => {
// Alert.alert("tag pressed", `You pressed the tag: ${tag}`);
// You can navigate to another screen or perform any other action here
//remove hastag from tag
tag = tag.replace("#", "");
navigation.navigate("Tags", { tag: tag });
};
const handleLinkPress = (url) => {
Linking.canOpenURL(url)
.then((supported) => {
if (supported) {
Linking.openURL(url);
} else {
Alert.alert('Error', 'Unable to open the link.');
}
})
.catch((err) => console.error('An error occurred', err));
};
const handleLinkLongPress = (url) => {
Share.share({
url: url
});
};
return (
<Card style={styles.card}>
<Card.Content style={{
@@ -72,27 +98,40 @@ let Post = (props) => {
margin: 0,
marginBottom: 0
}}>
<Hyperlink linkDefault={true} linkStyle={{ color: '#2980b9' }}>
{!post.nonOrganicType ?
<View>
<ProfilePhotoCircle profileid={post.profileid} />
<View style={{ flexDirection: 'row', alignItems: 'center', margin: 0, marginLeft: 37, marginTop: -14, paddingBottom: 2 }}>
{toProfileText}
</View>
{!post.nonOrganicType ?
<View>
<ProfilePhotoCircle profileid={post.profileid} />
<View style={{ flexDirection: 'row', alignItems: 'center', margin: 0, marginLeft: 37, marginTop: -14, paddingBottom: 2 }}>
{toProfileText}
<Text style={{ fontSize: 16, padding: 3, paddingLeft: 40 }}>{
cleanContent
}</Text>
<Media content={post.content} postId={post._id} post={post} style={{ paddingTop: 2 }} />
</View> :
<View>
<Chip icon="new-releases" style={{ width: 100 }} >{i18n.t("message.news")}</Chip>
<Text style={{ fontSize: 18 }}>{cleanContent}</Text>
<Media content={post.content} />
</View>
}
</Hyperlink>
<Pressable onLongPress={() => {
if (cleanContent.length > 10) {
Share.share({
message: cleanContent
});
}
}}>
<ParsedText
style={styles.text}
parse={[
{ pattern: /#(\w+)/, style: styles.tag, onPress: handleTagPress },
{ pattern: /(https?:\/\/[^\s]+)/, style: styles.link, onPress: handleLinkPress, onLongPress: handleLinkLongPress },
]}
>
{cleanContent}
</ParsedText>
</Pressable>
<Media content={post.content} postId={post._id} post={post} style={{ paddingTop: 2 }} />
</View> :
<View>
<Chip icon="new-releases" style={{ width: 100 }} >{i18n.t("message.news")}</Chip>
<Text style={{ fontSize: 18 }}>{cleanContent}</Text>
<Media content={post.content} />
</View>
}
</Card.Content>
<Card.Actions style={{ flexDirection: "row", flow: 4, fontSize: 16, marginLeft: 36, marginTop: -10 }}>
<Button
@@ -105,7 +144,13 @@ let Post = (props) => {
{likes}
</Button>
<Button icon="forum" labelStyle={{ fontSize: 17 }} style={{ flow: 1 }}
onPress={() => { changeshowCommentsB(!showCommentsB) }}
onPress={() => {
// changeshowCommentsB(!showCommentsB) // Show comments
// Change view to single post
navigation.navigate("SinglePost", {
postid: post._id,
});
}}
color="#555"
>
{post.comments.length}
@@ -133,12 +178,11 @@ let Post = (props) => {
{showCommentsB && <NewComment postid={post._id} newComentAdded={newComentAdded} />}
{
showCommentsB &&
<ScrollView style={{ maxHeight: 300 }}>
<FlatList
data={post.comments}
renderItem={renderComment}
keyExtractor={item => item.createdAt}
/></ScrollView>
<FlatList
data={post.comments}
renderItem={renderComment}
keyExtractor={item => item.createdAt}
/>
}
</Card>
);
@@ -164,5 +208,18 @@ const styles = StyleSheet.create({
margin: 8,
marginTop: 0,
padding: 8
}
},
text: {
fontSize: 16,
padding: 3,
paddingLeft: 40
},
tag: {
color: '#77B5FE',
textDecorationLine: 'underline',
},
link: {
color: '#77B5FE',
textDecorationLine: 'underline',
},
});

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Avatar } from 'react-native-paper';
import { View, StyleSheet, Text } from 'react-native';
import { View, StyleSheet, Text, TouchableOpacity } from 'react-native';
import API from './../API.js';
import { useNavigation } from '@react-navigation/native';
import { Image } from 'expo-image'; // Import Image from expo-image
@@ -29,13 +29,17 @@ const ProfileHeader = ({ profileid, withName = false, small = false }) => {
}
return (
<View style={styles.container}>
<TouchableOpacity onPress={onPress}>
<Image source={{ uri: photoUrl }} key={photoUrl}
style={{
width: small ? 25 : 35,
height: small ? 25 : 35,
aspectRatio: 1,
borderRadius: 50,
}} cachePolicy="memory-disk"/>
style={{
width: small ? 25 : 35,
height: small ? 25 : 35,
aspectRatio: 1,
borderRadius: 50,
}} cachePolicy="memory-disk"
/>
</TouchableOpacity>
<View style={styles.textContainer}>
<Text style={small ? styles.smallProfileName : styles.profileName} onPress={onPress}>{fullName}</Text>
</View>

View File

@@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react';
import { ScrollView } from 'react-native';
import { FlatList } from 'react-native';
import API from './../API.js';
import Post from './Post.js';
let SinglePostComponent = ({ postId }) => {
let SinglePostComponent = ({ postId, hideComments }) => {
let [post, setPost] = useState({});
useEffect(() => {
let subscribed = true;
@@ -19,9 +19,12 @@ let SinglePostComponent = ({ postId }) => {
}
}, [postId]);
return (post._id ? (
<ScrollView>
<Post post={post}/>
</ScrollView>
<FlatList
data={[post]}
renderItem={({ item }) => <Post post={item} showComments={hideComments ? false : true}/>}
keyExtractor={item => item._id}
/>
) : null);
};

14
package-lock.json generated
View File

@@ -40,6 +40,7 @@
"react-native-autoheight-webview": "^1.6.1",
"react-native-hyperlink": "0.0.19",
"react-native-paper": "^4.11.2",
"react-native-parsed-text": "^0.0.22",
"react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1",
"react-native-vector-icons": "^9.1.0",
@@ -13160,6 +13161,19 @@
"color-string": "^1.6.0"
}
},
"node_modules/react-native-parsed-text": {
"version": "0.0.22",
"resolved": "https://registry.npmjs.org/react-native-parsed-text/-/react-native-parsed-text-0.0.22.tgz",
"integrity": "sha512-hfD83RDXZf9Fvth3DowR7j65fMnlqM9PpxZBGWkzVcUTFtqe6/yPcIoIAgrJbKn6YmtzkivmhWE2MCE4JKBXrQ==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.7.x"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-safe-area-context": {
"version": "4.10.5",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.10.5.tgz",

View File

@@ -42,6 +42,7 @@
"react-native-autoheight-webview": "^1.6.1",
"react-native-hyperlink": "0.0.19",
"react-native-paper": "^4.11.2",
"react-native-parsed-text": "^0.0.22",
"react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1",
"react-native-vector-icons": "^9.1.0",