diff --git a/package.json b/package.json index e6c7fed05a..703548e566 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "react-dom": "17.0.2", "react-native": "0.68.2", "react-native-inappbrowser-reborn": "^3.6.3", + "react-native-progress": "^5.0.0", "react-native-safe-area-context": "^4.3.1", "react-native-screens": "^3.13.1", "react-native-svg": "^12.4.0", diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts index b3992544ba..bb3ef5d1ad 100644 --- a/src/state/lib/api.ts +++ b/src/state/lib/api.ts @@ -28,12 +28,46 @@ export async function setup(adx: AdxClient) { ) } +export async function post( + adx: AdxClient, + user: string, + text: string, + replyToUri?: string, +) { + let reply + if (replyToUri) { + const replyToUrip = new AdxUri(replyToUri) + const parentPost = await adx + .repo(replyToUrip.host, false) + .collection(replyToUrip.collection) + .get('Post', replyToUrip.recordKey) + if (parentPost) { + reply = { + root: parentPost.value.reply?.root || parentPost.uri, + parent: parentPost.uri, + } + } + } + return await adx + .repo(user, true) + .collection('blueskyweb.xyz:Posts') + .create('Post', { + $type: 'blueskyweb.xyz:Post', + text, + reply, + createdAt: new Date().toISOString(), + }) +} + export async function like(adx: AdxClient, user: string, uri: string) { - await adx.repo(user, true).collection('blueskyweb.xyz:Likes').create('Like', { - $type: 'blueskyweb.xyz:Like', - subject: uri, - createdAt: new Date().toISOString(), - }) + return await adx + .repo(user, true) + .collection('blueskyweb.xyz:Likes') + .create('Like', { + $type: 'blueskyweb.xyz:Like', + subject: uri, + createdAt: new Date().toISOString(), + }) } export async function unlike(adx: AdxClient, user: string, uri: string) { @@ -45,7 +79,7 @@ export async function unlike(adx: AdxClient, user: string, uri: string) { } export async function repost(adx: AdxClient, user: string, uri: string) { - await adx + return await adx .repo(user, true) .collection('blueskyweb.xyz:Posts') .create('Repost', { diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx new file mode 100644 index 0000000000..5a6ad52151 --- /dev/null +++ b/src/view/com/composer/Composer.tsx @@ -0,0 +1,88 @@ +import React, {useState, forwardRef, useImperativeHandle} from 'react' +import {observer} from 'mobx-react-lite' +import {KeyboardAvoidingView, StyleSheet, TextInput, View} from 'react-native' +// @ts-ignore no type definition -prf +import ProgressCircle from 'react-native-progress/Circle' +import {useStores} from '../../../state' +import {s} from '../../lib/styles' +import * as apilib from '../../../state/lib/api' + +const MAX_TEXT_LENGTH = 256 +const WARNING_TEXT_LENGTH = 200 +const DANGER_TEXT_LENGTH = 255 + +export const Composer = observer( + forwardRef(function Composer( + { + replyTo, + }: { + replyTo: string | undefined + }, + ref, + ) { + const store = useStores() + const [text, setText] = useState('') + + const onChangeText = (newText: string) => { + if (newText.length > MAX_TEXT_LENGTH) { + setText(newText.slice(0, MAX_TEXT_LENGTH)) + } else { + setText(newText) + } + } + + useImperativeHandle(ref, () => ({ + async publish() { + if (text.trim().length === 0) { + return false + } + await apilib.post(store.api, 'alice.com', text, replyTo) + return true + }, + })) + + const progressColor = + text.length > DANGER_TEXT_LENGTH + ? '#e60000' + : text.length > WARNING_TEXT_LENGTH + ? '#f7c600' + : undefined + + return ( + + onChangeText(text)} + value={text} + placeholder={ + replyTo ? 'Write your reply' : "What's new in the scene?" + } + style={styles.textInput} + /> + + + + + + + + ) + }), +) + +const styles = StyleSheet.create({ + outer: { + flexDirection: 'column', + backgroundColor: '#fff', + padding: 10, + height: '100%', + }, + textInput: { + flex: 1, + padding: 10, + }, +}) diff --git a/src/view/com/feed/FeedItem.tsx b/src/view/com/feed/FeedItem.tsx index 6ba1401c96..9f3ec7c56c 100644 --- a/src/view/com/feed/FeedItem.tsx +++ b/src/view/com/feed/FeedItem.tsx @@ -30,6 +30,11 @@ export const FeedItem = observer(function FeedItem({ name: item.author.name, }) } + const onPressReply = () => { + onNavigateContent('Composer', { + replyTo: item.uri, + }) + } const onPressToggleRepost = () => { item .toggleRepost() @@ -78,13 +83,13 @@ export const FeedItem = observer(function FeedItem({ {record.text} - + {item.replyCount} - + { + onNavigateContent('Composer', { + replyTo: item.uri, + }) + } const onPressToggleRepost = () => { item .toggleRepost() @@ -129,13 +134,13 @@ export const PostThreadItem = observer(function PostThreadItem({ <> )} - + {item.replyCount} - + = { prefixes: [ @@ -42,6 +43,7 @@ const linking: LinkingOptions = { PostThread: 'profile/:name/post/:recordKey', PostLikedBy: 'profile/:name/post/:recordKey/liked-by', PostRepostedBy: 'profile/:name/post/:recordKey/reposted-by', + Composer: 'compose', Login: 'login', Signup: 'signup', NotFound: '*', @@ -88,7 +90,8 @@ const HIDE_TAB = {tabBarButton: () => null} function HomeStackCom() { return ( - + + diff --git a/src/view/routes/types.ts b/src/view/routes/types.ts index fd58a7666e..2a9e1046a3 100644 --- a/src/view/routes/types.ts +++ b/src/view/routes/types.ts @@ -9,6 +9,7 @@ export type RootTabsParamList = { PostThread: {name: string; recordKey: string} PostLikedBy: {name: string; recordKey: string} PostRepostedBy: {name: string; recordKey: string} + Composer: {replyTo?: string} Login: undefined Signup: undefined NotFound: undefined diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx deleted file mode 100644 index 1b41b2d35d..0000000000 --- a/src/view/screens/Home.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, {useEffect} from 'react' -import {View} from 'react-native' -import {Shell} from '../shell' -import {Feed} from '../com/feed/Feed' -import type {RootTabsScreenProps} from '../routes/types' -import {useStores} from '../../state' - -export function Home({navigation}: RootTabsScreenProps<'HomeTab'>) { - const store = useStores() - useEffect(() => { - console.log('Fetching home feed') - store.homeFeed.setup() - }, [store.homeFeed]) - - const onNavigateContent = (screen: string, props: Record) => { - // @ts-ignore it's up to the callers to supply correct params -prf - navigation.navigate(screen, props) - } - - return ( - - - - - - ) -} diff --git a/src/view/screens/stacks/Composer.tsx b/src/view/screens/stacks/Composer.tsx new file mode 100644 index 0000000000..e1b36567ac --- /dev/null +++ b/src/view/screens/stacks/Composer.tsx @@ -0,0 +1,50 @@ +import React, {useLayoutEffect, useRef} from 'react' +import {Text, TouchableOpacity} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Shell} from '../../shell' +import type {RootTabsScreenProps} from '../../routes/types' +import {Composer as ComposerComponent} from '../../com/composer/Composer' + +export const Composer = ({ + navigation, + route, +}: RootTabsScreenProps<'Composer'>) => { + const {replyTo} = route.params + const ref = useRef<{publish: () => Promise}>() + + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: true, + headerTitle: replyTo ? 'Reply' : 'New Post', + headerLeft: () => ( + navigation.goBack()}> + + + ), + headerRight: () => ( + { + if (!ref.current) { + return + } + ref.current.publish().then( + posted => { + if (posted) { + navigation.goBack() + } + }, + err => console.error('Failed to create post', err), + ) + }}> + Post + + ), + }) + }, [navigation, replyTo, ref]) + + return ( + + + + ) +} diff --git a/src/view/screens/content/PostLikedBy.tsx b/src/view/screens/stacks/PostLikedBy.tsx similarity index 100% rename from src/view/screens/content/PostLikedBy.tsx rename to src/view/screens/stacks/PostLikedBy.tsx diff --git a/src/view/screens/content/PostRepostedBy.tsx b/src/view/screens/stacks/PostRepostedBy.tsx similarity index 100% rename from src/view/screens/content/PostRepostedBy.tsx rename to src/view/screens/stacks/PostRepostedBy.tsx diff --git a/src/view/screens/content/PostThread.tsx b/src/view/screens/stacks/PostThread.tsx similarity index 100% rename from src/view/screens/content/PostThread.tsx rename to src/view/screens/stacks/PostThread.tsx diff --git a/src/view/screens/content/Profile.tsx b/src/view/screens/stacks/Profile.tsx similarity index 100% rename from src/view/screens/content/Profile.tsx rename to src/view/screens/stacks/Profile.tsx diff --git a/src/view/screens/tabroots/Home.tsx b/src/view/screens/tabroots/Home.tsx new file mode 100644 index 0000000000..446a5a7e97 --- /dev/null +++ b/src/view/screens/tabroots/Home.tsx @@ -0,0 +1,59 @@ +import React, {useEffect, useLayoutEffect} from 'react' +import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Shell} from '../../shell' +import {Feed} from '../../com/feed/Feed' +import type {RootTabsScreenProps} from '../../routes/types' +import {useStores} from '../../../state' +import {AVIS} from '../../lib/assets' + +export function Home({navigation}: RootTabsScreenProps<'HomeTab'>) { + const store = useStores() + useEffect(() => { + console.log('Fetching home feed') + store.homeFeed.setup() + }, [store.homeFeed]) + + const onNavigateContent = (screen: string, props: Record) => { + // @ts-ignore it's up to the callers to supply correct params -prf + navigation.navigate(screen, props) + } + + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: true, + headerTitle: 'V I B E', + headerLeft: () => ( + navigation.push('Profile', {name: 'alice.com'})}> + + + ), + headerRight: () => ( + { + navigation.push('Composer', {}) + }}> + + + ), + }) + }, [navigation]) + + return ( + + + + + + ) +} + +const styles = StyleSheet.create({ + avi: { + width: 20, + height: 20, + borderRadius: 10, + resizeMode: 'cover', + }, +}) diff --git a/src/view/screens/Login.tsx b/src/view/screens/tabroots/Login.tsx similarity index 96% rename from src/view/screens/Login.tsx rename to src/view/screens/tabroots/Login.tsx index d08a5a2565..a5f670bdd8 100644 --- a/src/view/screens/Login.tsx +++ b/src/view/screens/tabroots/Login.tsx @@ -1,7 +1,7 @@ import React from 'react' import {Text, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {Shell} from '../shell' +import {Shell} from '../../shell' // import type {RootTabsScreenProps} from '../routes/types' // import {useStores} from '../../state' diff --git a/src/view/screens/Menu.tsx b/src/view/screens/tabroots/Menu.tsx similarity index 81% rename from src/view/screens/Menu.tsx rename to src/view/screens/tabroots/Menu.tsx index d0cc0826fb..dca5ad33be 100644 --- a/src/view/screens/Menu.tsx +++ b/src/view/screens/tabroots/Menu.tsx @@ -1,7 +1,7 @@ import React from 'react' -import {Shell} from '../shell' +import {Shell} from '../../shell' import {ScrollView, Text, View} from 'react-native' -import type {RootTabsScreenProps} from '../routes/types' +import type {RootTabsScreenProps} from '../../routes/types' export const Menu = (_props: RootTabsScreenProps<'MenuTab'>) => { return ( diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/tabroots/NotFound.tsx similarity index 82% rename from src/view/screens/NotFound.tsx rename to src/view/screens/tabroots/NotFound.tsx index 5357a428a7..a35808cbc6 100644 --- a/src/view/screens/NotFound.tsx +++ b/src/view/screens/tabroots/NotFound.tsx @@ -1,7 +1,7 @@ import React from 'react' -import {Shell} from '../shell' +import {Shell} from '../../shell' import {Text, Button, View} from 'react-native' -import type {RootTabsScreenProps} from '../routes/types' +import type {RootTabsScreenProps} from '../../routes/types' export const NotFound = ({navigation}: RootTabsScreenProps<'NotFound'>) => { return ( diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/tabroots/Notifications.tsx similarity index 79% rename from src/view/screens/Notifications.tsx rename to src/view/screens/tabroots/Notifications.tsx index a7918f1776..091410ad8c 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/tabroots/Notifications.tsx @@ -1,7 +1,7 @@ import React from 'react' -import {Shell} from '../shell' +import {Shell} from '../../shell' import {Text, View} from 'react-native' -import type {RootTabsScreenProps} from '../routes/types' +import type {RootTabsScreenProps} from '../../routes/types' export const Notifications = ( _props: RootTabsScreenProps<'NotificationsTab'>, diff --git a/src/view/screens/Search.tsx b/src/view/screens/tabroots/Search.tsx similarity index 78% rename from src/view/screens/Search.tsx rename to src/view/screens/tabroots/Search.tsx index 26df2954c0..044ca749c7 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/tabroots/Search.tsx @@ -1,7 +1,7 @@ import React from 'react' -import {Shell} from '../shell' +import {Shell} from '../../shell' import {Text, View} from 'react-native' -import type {RootTabsScreenProps} from '../routes/types' +import type {RootTabsScreenProps} from '../../routes/types' export const Search = (_props: RootTabsScreenProps<'SearchTab'>) => { return ( diff --git a/src/view/screens/Signup.tsx b/src/view/screens/tabroots/Signup.tsx similarity index 96% rename from src/view/screens/Signup.tsx rename to src/view/screens/tabroots/Signup.tsx index 4a8c5df2d0..dc2af2b1e6 100644 --- a/src/view/screens/Signup.tsx +++ b/src/view/screens/tabroots/Signup.tsx @@ -1,7 +1,7 @@ import React from 'react' import {Text, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {Shell} from '../shell' +import {Shell} from '../../shell' // import type {RootTabsScreenProps} from '../routes/types' // import {useStores} from '../../state' diff --git a/todos.txt b/todos.txt index 7e38d5302d..e170e38e96 100644 --- a/todos.txt +++ b/todos.txt @@ -6,12 +6,13 @@ Paul's todo list - Thread view - Refresh - Share btn - - Reply control - Profile view - Refresh - Follow / Unfollow - Badges -- Compose post control +- Composer + - Refresh original view after reply + - Check on navigation stack during a bunch of replies - Search view - * - Notifications view diff --git a/yarn.lock b/yarn.lock index 581f59f692..b15d85f35f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11146,6 +11146,13 @@ react-native-inappbrowser-reborn@^3.6.3: invariant "^2.2.4" opencollective-postinstall "^2.0.2" +react-native-progress@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/react-native-progress/-/react-native-progress-5.0.0.tgz#f5ac6ceaeee27f184c660b00f29419e82a9d0ab0" + integrity sha512-KjnGIt3r9i5Kn2biOD9fXLJocf0bwxPRxOyAgXEnZTJQU2O+HyzgGFRCbM5h3izm9kKIkSc1txh8aGmMafCD9A== + dependencies: + prop-types "^15.7.2" + react-native-safe-area-context@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.3.1.tgz#5cf97b25b395e0d09bc1f828920cd7da0d792ade"