From ffce18adb0215cf8e02c1359731e2d7bbfdb5199 Mon Sep 17 00:00:00 2001 From: Micah Lindley Date: Tue, 12 Mar 2024 02:20:51 -0500 Subject: [PATCH] Switch to Sidechat.js and add group list to user page --- .github/workflows/android-debug.yml | 4 +- android/app/build.gradle | 4 +- docs/latest.json | 2 +- package-lock.json | 61 +++-- package.json | 3 +- src/App.jsx | 19 +- src/components/Comment.jsx | 14 +- src/components/CommentModal.jsx | 10 +- src/components/Post.jsx | 11 +- src/screens/GroupsScreen.jsx | 102 ++++++++ src/screens/HomeScreen.jsx | 62 ++--- src/screens/LoginScreen.jsx | 17 +- src/screens/MyProfileScreen.jsx | 98 +++++--- src/screens/SettingsScreen.jsx | 5 +- src/types/OffsidesTypes.js | 14 ++ src/utils/index.js | 26 ++ src/utils/sidechatAPI.js | 366 ---------------------------- yarn.lock | 63 ++--- 18 files changed, 366 insertions(+), 515 deletions(-) create mode 100644 src/screens/GroupsScreen.jsx create mode 100644 src/types/OffsidesTypes.js create mode 100644 src/utils/index.js delete mode 100644 src/utils/sidechatAPI.js diff --git a/.github/workflows/android-debug.yml b/.github/workflows/android-debug.yml index 69652c2..0d455a9 100644 --- a/.github/workflows/android-debug.yml +++ b/.github/workflows/android-debug.yml @@ -48,13 +48,13 @@ jobs: echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV - name: Rename APK - run: mv android/app/build/outputs/apk/release/app-release.apk android/app/build/outputs/apk/release/offsides-nightly-${{ env.COMMIT_SHORT_SHA }}.apk + run: mv android/app/build/outputs/apk/release/app-release.apk offsides-nightly-${{ env.COMMIT_SHORT_SHA }}.apk - name: Create prerelease uses: ncipollo/release-action@v1.14.0 with: name: Offsides [NIGHTLY] - ${{ env.COMMIT_SHORT_SHA }} - artifacts: android/app/build/outputs/apk/release/offsides-nightly-${{ env.COMMIT_SHORT_SHA }}.apk + artifacts: offsides-nightly-${{ env.COMMIT_SHORT_SHA }}.apk body: This is a nightly release - do not use if you are not prepared for bugs and glitches. Instead, try the latest versioned release. Created from commit ${{ github.sha }}. prerelease: true allowUpdates: true diff --git a/android/app/build.gradle b/android/app/build.gradle index a4b7ce0..bc1afc1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -79,8 +79,8 @@ android { applicationId "com.micahlindley.offsides" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 9 - versionName "0.3.4" + versionCode 10 + versionName "0.3.7" } signingConfigs { debug { diff --git a/docs/latest.json b/docs/latest.json index 671cdd0..6950c7c 100644 --- a/docs/latest.json +++ b/docs/latest.json @@ -1,3 +1,3 @@ { - "latestVersion": "0.3.4" + "latestVersion": "0.3.7" } diff --git a/package-lock.json b/package-lock.json index 319ab84..167a806 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "offsides", - "version": "0.3.4", + "version": "0.3.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "offsides", - "version": "0.3.4", + "version": "0.3.7", "dependencies": { "@pchmn/expo-material3-theme": "^1.3.2", "@react-native-async-storage/async-storage": "^1.22.3", @@ -27,6 +27,7 @@ "react-native-storage": "^1.0.1", "react-native-vector-icons": "^10.0.3", "semver": "^7.6.0", + "sidechat.js": "^2.1.0", "timesago": "^1.0.1" }, "devDependencies": { @@ -2569,16 +2570,16 @@ } }, "node_modules/@expo/cli": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.17.6.tgz", - "integrity": "sha512-vpwQOyhkqQ5Ao96AGaFntRf6dX7h7/e9T7oKZ5KfJiaLRgfmNa/yHFu5cpXG76T2R7Q6aiU4ik0KU3P7nFMzEw==", + "version": "0.17.7", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.17.7.tgz", + "integrity": "sha512-sOssVCFCVXSdZr2/KdqPeT2Qwxmty3rZeO9g5RbzZexHz93VUyONuqGwO1VlYKibn7FLYEGUovqU9Xi8zVB6JQ==", "dependencies": { "@babel/runtime": "^7.20.0", "@expo/code-signing-certificates": "0.0.5", "@expo/config": "~8.5.0", "@expo/config-plugins": "~7.8.0", "@expo/devcert": "^1.0.0", - "@expo/env": "~0.2.0", + "@expo/env": "~0.2.2", "@expo/image-utils": "^0.4.0", "@expo/json-file": "^8.2.37", "@expo/metro-config": "~0.17.0", @@ -3086,9 +3087,9 @@ "integrity": "sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==" }, "node_modules/@expo/env": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-0.2.1.tgz", - "integrity": "sha512-deZmRS7Dvp18VM8s559dq/ZjPlV1D9vtLoLXwHmCK/JYOvtNptdKsfxcWjI7ewmo6ln2PqgNI9HRI74q6Wk2eA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-0.2.2.tgz", + "integrity": "sha512-m9nGuaSpzdvMzevQ1H60FWgf4PG5s4J0dfKUzdAGnDu7sMUerY/yUeDaA4+OBo3vBwGVQ+UHcQS9vPSMBNaPcg==", "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", @@ -3303,16 +3304,16 @@ } }, "node_modules/@expo/metro-config": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.17.5.tgz", - "integrity": "sha512-2YUebeIwr6gFxcIRSVAjWK5D8YSaXBzQoRRl3muJWsH8AC8a+T60xbA3cGhsEICD2zKS5zwnL2yobgs41Ur7nQ==", + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.17.6.tgz", + "integrity": "sha512-WaC1C+sLX/Wa7irwUigLhng3ckmXIEQefZczB8DfYmleV6uhfWWo2kz/HijFBpV7FKs2cW6u8J/aBQpFkxlcqg==", "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "@expo/config": "~8.5.0", - "@expo/env": "~0.2.0", + "@expo/env": "~0.2.2", "@expo/json-file": "~8.3.0", "@expo/spawn-async": "^1.7.2", "babel-preset-fbjs": "^3.4.0", @@ -8507,23 +8508,23 @@ } }, "node_modules/expo": { - "version": "50.0.8", - "resolved": "https://registry.npmjs.org/expo/-/expo-50.0.8.tgz", - "integrity": "sha512-8yXsoMbFRjWyEDNuFRtH0vTFvEjFnnwP+LceS6xmXGp+IW1hKdN1X6Bj1EUocFtepH0ruHDPCof1KvPoWfUWkw==", + "version": "50.0.12", + "resolved": "https://registry.npmjs.org/expo/-/expo-50.0.12.tgz", + "integrity": "sha512-GqngGj+BRjapzZ+/Ve3kFCHDdiGFPZ3FsIkd034aYKVExjxI298vOeGkfQsh/CpSpSpBvnt10XMYdgDn+RwyVQ==", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "0.17.6", + "@expo/cli": "0.17.7", "@expo/config": "8.5.4", "@expo/config-plugins": "7.8.4", - "@expo/metro-config": "0.17.5", + "@expo/metro-config": "0.17.6", "@expo/vector-icons": "^14.0.0", "babel-preset-expo": "~10.0.1", "expo-asset": "~9.0.2", - "expo-file-system": "~16.0.7", + "expo-file-system": "~16.0.8", "expo-font": "~11.10.3", "expo-keep-awake": "~12.8.2", "expo-modules-autolinking": "1.10.3", - "expo-modules-core": "1.11.9", + "expo-modules-core": "1.11.11", "fbemitter": "^3.0.0", "whatwg-url-without-unicode": "8.0.0-3" }, @@ -8556,9 +8557,9 @@ } }, "node_modules/expo-file-system": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.7.tgz", - "integrity": "sha512-BELr1Agj6WK0PKVMcD0rqC3fP5unKfp2KW8/sNhtTHgdzQ/F0Pylq9pTk9u7KEu0ZbEdTpk5EMarLMPwffi3og==", + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.8.tgz", + "integrity": "sha512-yDbVT0TUKd7ewQjaY5THum2VRFx2n/biskGhkUmLh3ai21xjIVtaeIzHXyv9ir537eVgt4ReqDNWi7jcXjdUcA==", "peerDependencies": { "expo": "*" } @@ -8683,9 +8684,9 @@ } }, "node_modules/expo-modules-core": { - "version": "1.11.9", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.11.9.tgz", - "integrity": "sha512-GTUb81vcPaF+5MtlBI1u9IjrZbGdF1ZUwz3u8Gc+rOLBblkZ7pYsj2mU/tu+k0khTckI9vcH4ZBksXWvE1ncjQ==", + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.11.11.tgz", + "integrity": "sha512-c4SmfHfLV/HthYLJT16NJhUZ1lVV8XP4UImIabdvKQQ8MGiFnFytVX+Jf8rm2uGBDbzz6zgEbNITeio14mdUhg==", "dependencies": { "invariant": "^2.2.4" } @@ -14604,6 +14605,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sidechat.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sidechat.js/-/sidechat.js-2.1.0.tgz", + "integrity": "sha512-C2+MuVe0SzowiSWRF4b4RkO935tMU6vzPhAPOezf14la2yifdkc8v5HgsUmVFvp3+3TCe4rWogHRRFN37hmDyA==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", diff --git a/package.json b/package.json index 6964b9b..8e17ea1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "offsides", - "version": "0.3.4", + "version": "0.3.7", "private": true, "scripts": { "android": "react-native run-android", @@ -29,6 +29,7 @@ "react-native-storage": "^1.0.1", "react-native-vector-icons": "^10.0.3", "semver": "^7.6.0", + "sidechat.js": "^2.1.0", "timesago": "^1.0.1" }, "devDependencies": { diff --git a/src/App.jsx b/src/App.jsx index 22c33f1..2bdc4f2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,31 +1,43 @@ import 'react-native-gesture-handler'; -import React from 'react'; +import React, { Context } from 'react'; +import { OffsidesAppState } from './types/OffsidesTypes'; import { StatusBar, useColorScheme } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { SidechatAPIClient } from 'sidechat.js'; import HomeScreen from './screens/HomeScreen'; import SettingsScreen from './screens/SettingsScreen'; import LoginScreen from './screens/LoginScreen'; import MyProfileScreen from './screens/MyProfileScreen'; import CommentModal from './components/CommentModal'; +import GroupsScreen from './screens/GroupsScreen'; const Stack = createNativeStackNavigator(); +/** + * Global app context for Offsides. Contains API as well as current app state. + * @type {Context} + */ const AppContext = React.createContext(); export default function App() { const colorScheme = useColorScheme(); const [needsLogin, setNeedsLogin] = React.useState(null); const [appState, setAppState] = React.useState({}); + React.useEffect(() => { AsyncStorage.multiGet(['userToken', 'userID', 'groupID', 'groupName']).then( res => { - let tempState = appState; + let tempState = {}; // If user token is defined if (res[0][1]) { + tempState.API = new SidechatAPIClient(res[0][1]); setNeedsLogin(false); - } else setNeedsLogin(true); + } else { + tempState.API = new SidechatAPIClient(); + setNeedsLogin(true); + } res.forEach(item => { tempState[item[0]] = item[1]; }); @@ -47,6 +59,7 @@ export default function App() { + { const action = vote == 'upvote' ? 'none' : 'upvote'; - API.setVote(comment.id, appState.userToken, action).then(res => { + API.setVote(comment.id, action).then(res => { setVote(action); setVoteCount(res.post.vote_total); }); @@ -25,7 +29,7 @@ function Comment({ comment, nav }) { const downvote = () => { const action = vote == 'downvote' ? 'none' : 'downvote'; - API.setVote(comment.id, appState.userToken, action).then(res => { + API.setVote(comment.id, action).then(res => { setVote(action); setVoteCount(res.post.vote_total); }); @@ -95,7 +99,7 @@ function Comment({ comment, nav }) { )} diff --git a/src/components/CommentModal.jsx b/src/components/CommentModal.jsx index 86b11ae..43d6489 100644 --- a/src/components/CommentModal.jsx +++ b/src/components/CommentModal.jsx @@ -8,15 +8,17 @@ import { } from 'react-native'; import { Appbar, useTheme, Text, Divider } from 'react-native-paper'; import { AppContext } from '../App'; -import * as API from '../utils/sidechatAPI'; import Comment from './Comment'; import Post from './Post'; function CommentModal({ navigation, route }) { + /** @type {{postID: String, postObj: SidechatPostOrComment}} */ const { postID, postObj } = route.params; - const appState = React.useContext(AppContext); + const { API } = React.useContext(AppContext); const { colors } = useTheme(); - const [comments, setComments] = React.useState([]); + const [comments, setComments] = React.useState( + /** @type {SidechatPostOrComment[]} */ ([]), + ); const [loadingComments, setLoadingComments] = React.useState(true); useEffect(() => { @@ -30,7 +32,7 @@ function CommentModal({ navigation, route }) { ); const fetchComments = () => { setLoadingComments(true); - API.getPostComments(postID, appState.userToken).then(res => { + API.getPostComments(postID).then(res => { setComments(res); setLoadingComments(false); }); diff --git a/src/components/Post.jsx b/src/components/Post.jsx index 1d18708..5e10629 100644 --- a/src/components/Post.jsx +++ b/src/components/Post.jsx @@ -1,15 +1,14 @@ import React from 'react'; -import { Image, View } from 'react-native'; +import { View } from 'react-native'; import { Avatar, Card, IconButton, Text, useTheme } from 'react-native-paper'; import timesago from 'timesago'; import { AppContext } from '../App'; -import * as API from '../utils/sidechatAPI'; import AutoImage from './AutoImage'; const BORDER_RADIUS = 12; function Post({ post, nav, commentView = false }) { - const appState = React.useContext(AppContext); + const { API } = React.useContext(AppContext); const { colors } = useTheme(); const [vote, setVote] = React.useState(post.vote_status); const [voteCount, setVoteCount] = React.useState(post.vote_total); @@ -17,7 +16,7 @@ function Post({ post, nav, commentView = false }) { const upvote = () => { const action = vote == 'upvote' ? 'none' : 'upvote'; - API.setVote(post.id, appState.userToken, action).then(res => { + API.setVote(post.id, action).then(res => { setVote(action); setVoteCount(res.post.vote_total); }); @@ -25,7 +24,7 @@ function Post({ post, nav, commentView = false }) { const downvote = () => { const action = vote == 'downvote' ? 'none' : 'downvote'; - API.setVote(post.id, appState.userToken, action).then(res => { + API.setVote(post.id, action).then(res => { setVote(action); setVoteCount(res.post.vote_total); }); @@ -72,7 +71,7 @@ function Post({ post, nav, commentView = false }) { { + API.getUserAndGroup(appState.userToken, appState.groupID).then(d => { + setData(d); + setLoading(false); + }); + }, []); + return ( + + + + navigation.goBack()} /> + + navigation.navigate('Settings')} + icon="cog" + /> + + + {data?.user && ( + + + + + {data.groups.map(group => ( + + + {group.name} + + } + descriptionStyle={{ marginTop: 10 }} + description={ + <> + + {group.membership_type[0].toUpperCase() + + group.membership_type.slice(1)}{' '} + •{' '} + {group.group_visibility[0].toUpperCase() + + group.group_visibility.slice(1)}{' '} + group + + + } + onPress={() => {}} + /> + ))} + + + + )} + + ); +} + +export default GroupsScreen; diff --git a/src/screens/HomeScreen.jsx b/src/screens/HomeScreen.jsx index bbeaf50..ff60f49 100644 --- a/src/screens/HomeScreen.jsx +++ b/src/screens/HomeScreen.jsx @@ -1,3 +1,7 @@ +import { + SidechatCursorString, + SidechatPostOrComment, +} from 'sidechat.js/src/types'; import React from 'react'; import { View, @@ -13,22 +17,28 @@ import { useTheme, Menu, ProgressBar, + TouchableRipple, } from 'react-native-paper'; import { AppContext } from '../App'; import Post from '../components/Post'; -import * as API from '../utils/sidechatAPI'; const BORDER_RADIUS = 12; function HomeScreen({ navigation }) { const appState = React.useContext(AppContext); + const API = appState.API; const [postCategory, setPostCategory] = React.useState('hot'); - const [cursor, setCursor] = React.useState(null); + + const [cursor, setCursor] = React.useState( + /** @type {SidechatCursorString} */ (null), + ); const { colors } = useTheme(); const [filterOpen, setFilterOpen] = React.useState(false); const [loadingPosts, setLoadingPosts] = React.useState(true); const [renderedPostIds, setRenderedPostIds] = React.useState(new Set()); - const [posts, setPosts] = React.useState([]); + const [posts, setPosts] = React.useState( + /** @type {SidechatPostOrComment[]} */ ([]), + ); React.useEffect(() => { InteractionManager.runAfterInteractions(() => { if (appState.groupID && appState.userToken) { @@ -55,22 +65,13 @@ function HomeScreen({ navigation }) { setLoadingPosts(true); if (refresh) { setPosts([]); - API.getGroupPosts( - appState.groupID, - appState.userToken, - postCategory, - ).then(res => { + API.getGroupPosts(appState.groupID, postCategory).then(res => { setPosts(res.posts); setCursor(res.cursor); setLoadingPosts(false); }); } else { - API.getGroupPosts( - appState.groupID, - appState.userToken, - postCategory, - cursor, - ).then(res => { + API.getGroupPosts(appState.groupID, postCategory, cursor).then(res => { setPosts(posts.concat(res.posts)); setCursor(res.cursor); setLoadingPosts(false); @@ -85,20 +86,22 @@ function HomeScreen({ navigation }) { - + navigation.navigate('Groups')}> + + {appState.groupName.length > 2 ? ( {appState.groupName} ) : ( @@ -161,9 +164,6 @@ function HomeScreen({ navigation }) { navigation.push('MyProfile')}> - navigation.push('Settings')}> )} diff --git a/src/screens/LoginScreen.jsx b/src/screens/LoginScreen.jsx index 7f07d1a..1bde78b 100644 --- a/src/screens/LoginScreen.jsx +++ b/src/screens/LoginScreen.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { View, StyleSheet } from 'react-native'; import { Button, @@ -10,9 +10,12 @@ import { } from 'react-native-paper'; import AsyncStorage from '@react-native-async-storage/async-storage'; import RNRestart from 'react-native-restart'; -import * as API from '../utils/sidechatAPI'; +import { AppContext } from '../App'; +import DeviceInfo from 'react-native-device-info'; +import { sha256 } from 'js-sha256'; function LoginScreen({}) { + const { API } = useContext(AppContext); const { colors } = useTheme(); const [errorMessage, setErrorMessage] = React.useState(); const [phase, setPhase] = React.useState('sendSMS'); @@ -22,7 +25,6 @@ function LoginScreen({}) { const [myAge, setMyAge] = React.useState(); const [email, setEmail] = React.useState(); const [loading, setLoading] = React.useState(false); - const [localToken, setLocalToken] = React.useState(null); const sendSMS = async () => { setLoading(true); try { @@ -97,9 +99,10 @@ function LoginScreen({}) { throw new Error(res.message); } else { if (res.token) { - setLocalToken(res.token); await AsyncStorage.setItem('userToken', res.token); - await API.setDeviceID(res.token); + const id = await DeviceInfo.getAndroidId(); + const deviceID = sha256(id); + await API.setDeviceID(deviceID); setPhase('registerEmail'); } else { throw new Error('Failed to set age.'); @@ -115,7 +118,7 @@ function LoginScreen({}) { const registerEmail = async () => { setLoading(true); try { - const res = await API.registerEmail(email, localToken); + const res = await API.registerEmail(email); if (res) { if (res.error_code) { throw new Error(res.message); @@ -133,7 +136,7 @@ function LoginScreen({}) { const verifyEmail = async () => { setLoading(true); try { - const res = await API.checkEmailVerification(localToken); + const res = await API.checkEmailVerification(); if (res) { if (res.error_code) { throw new Error(res.message); diff --git a/src/screens/MyProfileScreen.jsx b/src/screens/MyProfileScreen.jsx index 252d757..920d423 100644 --- a/src/screens/MyProfileScreen.jsx +++ b/src/screens/MyProfileScreen.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View, StatusBar, ScrollView } from 'react-native'; +import { View, StatusBar, ScrollView, Image } from 'react-native'; import { Appbar, useTheme, @@ -10,22 +10,37 @@ import { ProgressBar, } from 'react-native-paper'; import { AppContext } from '../App'; -import * as API from '../utils/sidechatAPI'; import timesago from 'timesago'; const BORDER_RADIUS = 15; function MyProfileScreen({ navigation }) { const appState = React.useContext(AppContext); - const [data, setData] = React.useState(false); + const API = appState.API; + const [updates, setUpdates] = React.useState(false); + const [groups, setGroups] = React.useState(false); const [loading, setLoading] = React.useState(true); const { colors } = useTheme(); React.useEffect(() => { - API.getUserAndGroup(appState.groupID, appState.userToken).then(d => { - setData(d); + loadProfile(); + }, []); + const loadProfile = async () => { + const group = await API.getUpdates(appState.groupID); + const user = await API.getCurrentUser(); + Promise.all( + user.memberships.map(m => { + return API.getGroupMetadata(m.groupId); + }), + ).then(data => { + data = data.filter(g => { + if (g) return true; + else return false; + }); + setGroups(data); + setUpdates(group); setLoading(false); }); - }, []); + }; return ( @@ -38,17 +53,17 @@ function MyProfileScreen({ navigation }) { /> - {data?.user && ( + {updates?.user && ( - {data.user?.conversation_icon ? ( + {updates.user?.conversation_icon ? ( - joined {timesago(data.user.created_at)} + joined {timesago(updates.user.created_at)} @@ -74,7 +89,7 @@ function MyProfileScreen({ navigation }) { titleStyle={{ minHeight: 10 }} /> - {data.user.follower_count} + {updates.user.follower_count} @@ -84,7 +99,9 @@ function MyProfileScreen({ navigation }) { titleStyle={{ minHeight: 10 }} /> - {data.karma.groups[0].yakarma} + + {updates.karma.groups[0].yakarma} + @@ -95,31 +112,50 @@ function MyProfileScreen({ navigation }) { titleStyle={{ minHeight: 10 }} /> - {data.groups.map(group => ( + {groups.map(group => ( - + {group.icon_url ? ( + + ) : ( + + )} {group.name} } @@ -131,7 +167,9 @@ function MyProfileScreen({ navigation }) { group.membership_type.slice(1)}{' '} •{' '} {group.group_visibility[0].toUpperCase() + - group.group_visibility.slice(1)}{' '} + group.group_visibility + .slice(1) + .replaceAll('_', ' ')}{' '} group diff --git a/src/screens/SettingsScreen.jsx b/src/screens/SettingsScreen.jsx index 7956397..847eb74 100644 --- a/src/screens/SettingsScreen.jsx +++ b/src/screens/SettingsScreen.jsx @@ -12,8 +12,8 @@ import RNRestart from 'react-native-restart'; import { version } from '../../package.json'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { AppContext } from '../App'; -import * as API from '../utils/sidechatAPI'; import offsidesLogo from '../assets/Offsides.png'; +import { needsUpdate } from '../utils'; function SettingsScreen({ navigation }) { const appState = React.useContext(AppContext); @@ -23,7 +23,8 @@ function SettingsScreen({ navigation }) { checkForUpdate(); }, []); const checkForUpdate = async () => { - setUpdateAvailable(await API.needsUpdate(version)); + const needs = await needsUpdate(version); + setUpdateAvailable(needs); }; const signOut = () => { AsyncStorage.clear().then(() => RNRestart.restart()); diff --git a/src/types/OffsidesTypes.js b/src/types/OffsidesTypes.js new file mode 100644 index 0000000..52c2d4c --- /dev/null +++ b/src/types/OffsidesTypes.js @@ -0,0 +1,14 @@ +import { SidechatAuthToken } from 'sidechat.js/src/types'; +import { SidechatAPIClient } from 'sidechat.js'; + +/** + * The app state as passed via React Context + * @typedef {Object} OffsidesAppState + * @prop {SidechatAPIClient} API - a reference to the global API client. Optionally you can instantiate a local instance with the same user by accessing `API.userToken` + * @prop {SidechatAuthToken} userToken - the logged-in user's bearer token + * @prop {String} userID - the logged-in user's alphanumeric ID + * @prop {String} groupID - the currently selected group ID + * @prop {String} groupName - the currently selected group name + */ + +export default {}; diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..e448e0f --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,26 @@ +import semver from 'semver'; + +/** + * Finds out if the user is running the latest version of Offsides + * @param {String} currentVersion - semver current version + * @returns {Boolean|String} if app is up to date, false; if app needs an update, the semver of the latest version + */ +const needsUpdate = async currentVersion => { + try { + const res = await fetch(`https://offsides.micahlindley.com/latest.json`); + if (res.ok) { + const json = await res.json(); + if (semver.gt(json.latestVersion, currentVersion)) { + return json.latestVersion; + } else { + return false; + } + } else { + return false; + } + } catch { + return false; + } +}; + +export { needsUpdate }; diff --git a/src/utils/sidechatAPI.js b/src/utils/sidechatAPI.js deleted file mode 100644 index 7bd5b6e..0000000 --- a/src/utils/sidechatAPI.js +++ /dev/null @@ -1,366 +0,0 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { sha256 } from 'js-sha256'; -import DeviceInfo from 'react-native-device-info'; -const defaultHeaders = { - Accept: 'application/json', - 'Content-Type': 'application/json', -}; -import semver from 'semver'; - -/** - * Initiate the login process with a phone number. Should be followed up with verifySMSCode(). - * @param {Number} phoneNumber - US phone number (WITHOUT +1) to send verification code to - */ -const loginViaSMS = async phoneNumber => { - try { - const res = await fetch(`https://api.sidechat.lol/v1/login_register`, { - method: 'POST', - headers: defaultHeaders, - body: JSON.stringify({ - phone_number: `+1${phoneNumber}`, - version: 3, - }), - }); - const json = await res.json(); - return json; - } catch (err) { - console.err(error); - throw new Error('Failed to request SMS verification.'); - } -}; - -/** - * Verify the code sent via SMS with loginViaSMS(). - * @param {Number} phoneNumber - US phone number (WITHOUT +1) that verification code was sent to - * @param {String} code - the verification code - */ -const verifySMSCode = async (phoneNumber, code) => { - try { - const res = await fetch(`https://api.sidechat.lol/v1/verify_phone_number`, { - method: 'POST', - headers: defaultHeaders, - body: JSON.stringify({ - phone_number: `+1${phoneNumber}`, - code: code.toUpperCase(), - }), - }); - const json = await res.json(); - return json; - } catch (err) { - console.err(err); - throw new Error('Failed verify this code.'); - } -}; - -/** - * Set the user's age - * @param {Number} age - user's age in years - * @param {String} registrationID - the registration ID generated by verifySMSCode() - */ -const setAge = async (age, registrationID) => { - if (age < 13) { - throw new Error("You're too young to use Offsides."); - } - try { - const res = await fetch( - `https://api.sidechat.lol/v1/complete_registration`, - { - method: 'POST', - headers: defaultHeaders, - body: JSON.stringify({ - age: Number(age), - registration_id: registrationID, - }), - }, - ); - const json = await res.json(); - return json; - } catch (err) { - console.err(err); - throw new Error('Failed verify this code.'); - } -}; - -/** - * Initiate the email setup process. Should be followed up with checkEmailVerification(). - * @param {String} email - school email address to send verification code to - * @param {String} token - user bearer token - */ -const registerEmail = async (email, token) => { - try { - const res = await fetch( - `https://api.sidechat.lol/v2/users/register_email`, - { - method: 'POST', - headers: { - ...defaultHeaders, - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - email: email, - }), - }, - ); - const json = await res.json(); - if (json.message) { - throw new Error(json.message); - } - return json; - } catch (err) { - console.err(error); - throw new Error('Failed to request email verification.'); - } -}; - -/** - * Check is the user's email is verified. - * @param {String} token - user bearer token - */ -const checkEmailVerification = async token => { - try { - const res = await fetch( - `https://api.sidechat.lol/v1/users/check_email_verified`, - { - method: 'GET', - headers: { - ...defaultHeaders, - Authorization: `Bearer ${token}`, - }, - }, - ); - const json = await res.json(); - if (json.verified_email_updates_response) { - return json.verified_email_updates_response; - } else { - throw new Error(json?.message || 'Email is not verified.'); - } - } catch (err) { - console.err(error); - throw new Error('Email is not verified.'); - } -}; - -/** - * Set the device ID based on the Android device ID - * @param {String} token - the user bearer token - */ -const setDeviceID = async token => { - const id = await DeviceInfo.getAndroidId(); - const deviceID = sha256(id); - try { - const res = await fetch( - `https://api.sidechat.lol/v1/register_device_token`, - { - method: 'POST', - headers: { - ...defaultHeaders, - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - build_type: 'release', - bundle_id: 'com.flowerave.sidechat', - device_token: deviceID, - }), - }, - ); - const json = await res.json(); - await AsyncStorage.setItem('deviceID', deviceID); - return json; - } catch (err) { - console.err(err); - throw new Error('Failed verify this code.'); - } -}; - -/** - * Get updated status for user and group - * @param {String} groupID - the group ID that the user belongs to - * @param {String} token - the user bearer token - * @returns - */ -const getUserAndGroup = async (groupID, token) => { - try { - const res = await fetch( - `https://api.sidechat.lol/v1/updates?group_id=${groupID}`, - { - method: 'GET', - headers: { - ...defaultHeaders, - Authorization: `Bearer ${token}`, - }, - }, - ); - const json = await res.json(); - return json; - } catch (err) { - console.error(err); - throw new Error(`Failed to get posts from group.`); - } -}; - -/** - * Fetches posts from the specified category - * @param {String} groupID - * @param {String} token - * @param {"hot"|"recent"|"top"} category - * @param {String} [cursor] - * @returns List of posts - */ -const getGroupPosts = async (groupID, token, category = 'hot', cursor) => { - try { - const res = await fetch( - `https://api.sidechat.lol/v1/posts?group_id=${groupID}&type=${category}${ - cursor ? '&cursor=' + cursor : '' - }`, - { - method: 'GET', - headers: { - ...defaultHeaders, - Authorization: `Bearer ${token}`, - }, - }, - ); - const json = await res.json(); - return json; - } catch (err) { - console.error(err); - throw new Error(`Failed to get posts from group.`); - } -}; - -/** - * Upvote or downvote, or unvote a post - * @param {String} postID - post ID to vote on - * @param {String} token - user bearer token - * @param {"upvote"|"downvote"|"none"} action - whether to upvote, downvote, or reset vote - * @returns - */ -const setVote = async (postID, token, action) => { - try { - const res = await fetch(`https://api.sidechat.lol/v1/posts/set_vote`, { - method: 'POST', - headers: { - ...defaultHeaders, - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - post_id: postID, - vote_status: action, - }), - }); - const json = await res.json(); - return json; - } catch (err) { - console.error(err); - throw new Error(`Failed to change the vote on post.`); - } -}; - -/** - * Get all the commments on a post - * @param {*} postID - post ID to get comments for - * @param {*} token - user bearer token - * @returns - */ -const getPostComments = async (postID, token) => { - try { - const res = await fetch( - `https://api.sidechat.lol/v1/posts/comments/?post_id=${postID}`, - { - method: 'GET', - headers: { - ...defaultHeaders, - Authorization: `Bearer ${token}`, - }, - }, - ); - const json = await res.json(); - // Function to preprocess the comments and organize them into a nested structure - function preprocessComments(apiComments) { - // Map to store comments by their IDs for efficient lookup - const commentMap = new Map(); - // List to store top-level comments - const topLevelComments = []; - - // Iterate through the API comments - apiComments.forEach(comment => { - // Store the comment in the map with its ID as the key - commentMap.set(comment.id, comment); - // Get the parent comment using the reply_post_id - const parentComment = commentMap.get(comment.reply_post_id); - // Check if the comment is a top-level comment - if ( - !parentComment || - comment.reply_post_id === comment.parent_post_id - ) { - // If it's a top-level comment, push it to the topLevelComments array - topLevelComments.push(comment); - } else { - // If it's a reply, add it to the parent comment's replies array - if (!parentComment.replies) parentComment.replies = []; - parentComment.replies.push(comment); - } - }); - - // Flatten the nested structure and return a single list of comments - return flattenComments(topLevelComments); - } - - // Function to flatten nested comments into a single list - function flattenComments(comments) { - // Use reduce to flatten the nested comments array into a single list - return comments.reduce((flatComments, comment) => { - // Push the current comment to the flatComments array - flatComments.push(comment); - // If the current comment has replies, recursively flatten them and push to the flatComments array - if (comment.replies) - flatComments.push(...flattenComments(comment.replies)); - // Return the flatComments array - return flatComments; - }, []); - } - - const sortedComments = preprocessComments(json.posts); - return sortedComments; - } catch (err) { - console.error(err); - throw new Error(`Failed to get comments on post.`); - } -}; - -/** - * Finds out if the user is running the latest version of Offsides - * @param {String} currentVersion - semver current version - * @returns {Boolean|String} - if app is up to date, false; if app needs an update, the semver of the latest version - */ -const needsUpdate = async currentVersion => { - try { - const res = await fetch(`https://offsides.micahlindley.com/latest.json`); - if (res.ok) { - const json = await res.json(); - if (semver.gt(json.latestVersion, currentVersion)) { - return json.latestVersion; - } else { - return false; - } - } else { - return false; - } - } catch { - return false; - } -}; - -export { - loginViaSMS, - verifySMSCode, - setAge, - setDeviceID, - registerEmail, - checkEmailVerification, - getUserAndGroup, - getGroupPosts, - setVote, - getPostComments, - needsUpdate, -}; diff --git a/yarn.lock b/yarn.lock index 44619de..3db959c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1273,17 +1273,17 @@ mv "~2" safe-json-stringify "~1" -"@expo/cli@0.17.6": - version "0.17.6" - resolved "https://registry.npmjs.org/@expo/cli/-/cli-0.17.6.tgz" - integrity sha512-vpwQOyhkqQ5Ao96AGaFntRf6dX7h7/e9T7oKZ5KfJiaLRgfmNa/yHFu5cpXG76T2R7Q6aiU4ik0KU3P7nFMzEw== +"@expo/cli@0.17.7": + version "0.17.7" + resolved "https://registry.npmjs.org/@expo/cli/-/cli-0.17.7.tgz" + integrity sha512-sOssVCFCVXSdZr2/KdqPeT2Qwxmty3rZeO9g5RbzZexHz93VUyONuqGwO1VlYKibn7FLYEGUovqU9Xi8zVB6JQ== dependencies: "@babel/runtime" "^7.20.0" "@expo/code-signing-certificates" "0.0.5" "@expo/config" "~8.5.0" "@expo/config-plugins" "~7.8.0" "@expo/devcert" "^1.0.0" - "@expo/env" "~0.2.0" + "@expo/env" "~0.2.2" "@expo/image-utils" "^0.4.0" "@expo/json-file" "^8.2.37" "@expo/metro-config" "~0.17.0" @@ -1427,10 +1427,10 @@ tmp "^0.0.33" tslib "^2.4.0" -"@expo/env@~0.2.0": - version "0.2.1" - resolved "https://registry.npmjs.org/@expo/env/-/env-0.2.1.tgz" - integrity sha512-deZmRS7Dvp18VM8s559dq/ZjPlV1D9vtLoLXwHmCK/JYOvtNptdKsfxcWjI7ewmo6ln2PqgNI9HRI74q6Wk2eA== +"@expo/env@~0.2.2": + version "0.2.2" + resolved "https://registry.npmjs.org/@expo/env/-/env-0.2.2.tgz" + integrity sha512-m9nGuaSpzdvMzevQ1H60FWgf4PG5s4J0dfKUzdAGnDu7sMUerY/yUeDaA4+OBo3vBwGVQ+UHcQS9vPSMBNaPcg== dependencies: chalk "^4.0.0" debug "^4.3.4" @@ -1476,17 +1476,17 @@ json5 "^2.2.2" write-file-atomic "^2.3.0" -"@expo/metro-config@~0.17.0", "@expo/metro-config@0.17.5": - version "0.17.5" - resolved "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.17.5.tgz" - integrity sha512-2YUebeIwr6gFxcIRSVAjWK5D8YSaXBzQoRRl3muJWsH8AC8a+T60xbA3cGhsEICD2zKS5zwnL2yobgs41Ur7nQ== +"@expo/metro-config@~0.17.0", "@expo/metro-config@0.17.6": + version "0.17.6" + resolved "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.17.6.tgz" + integrity sha512-WaC1C+sLX/Wa7irwUigLhng3ckmXIEQefZczB8DfYmleV6uhfWWo2kz/HijFBpV7FKs2cW6u8J/aBQpFkxlcqg== dependencies: "@babel/core" "^7.20.0" "@babel/generator" "^7.20.5" "@babel/parser" "^7.20.0" "@babel/types" "^7.20.0" "@expo/config" "~8.5.0" - "@expo/env" "~0.2.0" + "@expo/env" "~0.2.2" "@expo/json-file" "~8.3.0" "@expo/spawn-async" "^1.7.2" babel-preset-fbjs "^3.4.0" @@ -4450,10 +4450,10 @@ expo-constants@~15.4.0: dependencies: "@expo/config" "~8.5.0" -expo-file-system@~16.0.0, expo-file-system@~16.0.7: - version "16.0.7" - resolved "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.7.tgz" - integrity sha512-BELr1Agj6WK0PKVMcD0rqC3fP5unKfp2KW8/sNhtTHgdzQ/F0Pylq9pTk9u7KEu0ZbEdTpk5EMarLMPwffi3og== +expo-file-system@~16.0.0, expo-file-system@~16.0.8: + version "16.0.8" + resolved "https://registry.npmjs.org/expo-file-system/-/expo-file-system-16.0.8.tgz" + integrity sha512-yDbVT0TUKd7ewQjaY5THum2VRFx2n/biskGhkUmLh3ai21xjIVtaeIzHXyv9ir537eVgt4ReqDNWi7jcXjdUcA== expo-font@~11.10.3: version "11.10.3" @@ -4479,31 +4479,31 @@ expo-modules-autolinking@>=0.8.1, expo-modules-autolinking@1.10.3: find-up "^5.0.0" fs-extra "^9.1.0" -expo-modules-core@^1.11.9, expo-modules-core@1.11.9: - version "1.11.9" - resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.11.9.tgz" - integrity sha512-GTUb81vcPaF+5MtlBI1u9IjrZbGdF1ZUwz3u8Gc+rOLBblkZ7pYsj2mU/tu+k0khTckI9vcH4ZBksXWvE1ncjQ== +expo-modules-core@^1.11.9, expo-modules-core@1.11.11: + version "1.11.11" + resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.11.11.tgz" + integrity sha512-c4SmfHfLV/HthYLJT16NJhUZ1lVV8XP4UImIabdvKQQ8MGiFnFytVX+Jf8rm2uGBDbzz6zgEbNITeio14mdUhg== dependencies: invariant "^2.2.4" expo@*, expo@^50.0.8: - version "50.0.8" - resolved "https://registry.npmjs.org/expo/-/expo-50.0.8.tgz" - integrity sha512-8yXsoMbFRjWyEDNuFRtH0vTFvEjFnnwP+LceS6xmXGp+IW1hKdN1X6Bj1EUocFtepH0ruHDPCof1KvPoWfUWkw== + version "50.0.12" + resolved "https://registry.npmjs.org/expo/-/expo-50.0.12.tgz" + integrity sha512-GqngGj+BRjapzZ+/Ve3kFCHDdiGFPZ3FsIkd034aYKVExjxI298vOeGkfQsh/CpSpSpBvnt10XMYdgDn+RwyVQ== dependencies: "@babel/runtime" "^7.20.0" - "@expo/cli" "0.17.6" + "@expo/cli" "0.17.7" "@expo/config" "8.5.4" "@expo/config-plugins" "7.8.4" - "@expo/metro-config" "0.17.5" + "@expo/metro-config" "0.17.6" "@expo/vector-icons" "^14.0.0" babel-preset-expo "~10.0.1" expo-asset "~9.0.2" - expo-file-system "~16.0.7" + expo-file-system "~16.0.8" expo-font "~11.10.3" expo-keep-awake "~12.8.2" expo-modules-autolinking "1.10.3" - expo-modules-core "1.11.9" + expo-modules-core "1.11.11" fbemitter "^3.0.0" whatwg-url-without-unicode "8.0.0-3" @@ -8089,6 +8089,11 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +sidechat.js@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/sidechat.js/-/sidechat.js-2.1.0.tgz" + integrity sha512-C2+MuVe0SzowiSWRF4b4RkO935tMU6vzPhAPOezf14la2yifdkc8v5HgsUmVFvp3+3TCe4rWogHRRFN37hmDyA== + signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"