diff --git a/src/app/(auth)/(tabs)/index.tsx b/src/app/(auth)/(tabs)/index.tsx
index 6a002428..f39e1c40 100644
--- a/src/app/(auth)/(tabs)/index.tsx
+++ b/src/app/(auth)/(tabs)/index.tsx
@@ -10,13 +10,7 @@ import { Text, View, XStack, Spinner, YStack } from 'tamagui'
import FeedPost from 'src/components/post/FeedPost'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context'
-import {
- type ErrorBoundaryProps,
- Stack,
- useLocalSearchParams,
- useNavigation,
- useRouter,
-} from 'expo-router'
+import { Stack, useLocalSearchParams, useNavigation, useRouter } from 'expo-router'
import {
useInfiniteQuery,
useMutation,
@@ -41,6 +35,8 @@ import { useVideo } from 'src/hooks/useVideoProvider'
import { useFocusEffect } from '@react-navigation/native'
import { useUserCache } from 'src/state/AuthProvider'
import type { Status } from 'src/lib/api-types'
+import type { ListRenderItemInfo } from 'react-native'
+import type { ErrorBoundaryProps } from 'expo-router'
export function ErrorBoundary(props: ErrorBoundaryProps) {
return (
@@ -159,12 +155,12 @@ export default function HomeScreen() {
const keyExtractor = useCallback((item) => item?.id, [])
- const onDeletePost = (id) => {
+ const onDeletePost = (id: string) => {
deletePostMutation.mutate(id)
}
const deletePostMutation = useMutation({
- mutationFn: async (id) => {
+ mutationFn: async (id: string) => {
return await deleteStatusV1(id)
},
onSuccess: (data, variables) => {
@@ -182,16 +178,16 @@ export default function HomeScreen() {
})
const bookmarkMutation = useMutation({
- mutationFn: async (id) => {
+ mutationFn: async (id: string) => {
return await postBookmark(id)
},
})
- const onBookmark = (id) => {
+ const onBookmark = (id: string) => {
bookmarkMutation.mutate(id)
}
- const onShare = (id, state) => {
+ const onShare = (id: string, state) => {
try {
shareMutation.mutate({ type: state == true ? 'unreblog' : 'reblog', id: id })
} catch (error) {
@@ -220,22 +216,22 @@ export default function HomeScreen() {
router.push(`/post/likes/${id}`)
}
- const handleGotoProfile = (id) => {
+ const handleGotoProfile = (id: string) => {
bottomSheetModalRef.current?.close()
router.push(`/profile/${id}`)
}
- const handleGotoUsernameProfile = (id) => {
+ const handleGotoUsernameProfile = (username: string) => {
bottomSheetModalRef.current?.close()
- router.push(`/profile/0?byUsername=${id}`)
+ router.push(`/profile/0?byUsername=${username}`)
}
- const gotoHashtag = (id) => {
+ const gotoHashtag = (id: string) => {
bottomSheetModalRef.current?.close()
router.push(`/hashtag/${id}`)
}
- const handleCommentReport = (id) => {
+ const handleCommentReport = (id: string) => {
bottomSheetModalRef.current?.close()
router.push(`/post/report/${id}`)
}
diff --git a/src/app/(auth)/(tabs)/profile.tsx b/src/app/(auth)/(tabs)/profile.tsx
index fe7aebd3..6d0511af 100644
--- a/src/app/(auth)/(tabs)/profile.tsx
+++ b/src/app/(auth)/(tabs)/profile.tsx
@@ -40,7 +40,10 @@ export default function ProfileScreen() {
} = useInfiniteQuery({
queryKey: ['statusesById', userId],
queryFn: async ({ pageParam }) => {
- const data = await getAccountStatusesById(userId, pageParam)
+ if (!userId) {
+ throw new Error('getAccountStatusesById: user id missing')
+ }
+ const data = await getAccountStatusesById(userId, { max_id: pageParam })
return data.filter((p) => {
return (
['photo', 'photo:album', 'video'].includes(p.pf_type) &&
@@ -111,6 +114,7 @@ export default function ProfileScreen() {
)}
+
item?.id.toString()}
diff --git a/src/app/(auth)/admin/users/show/[id].tsx b/src/app/(auth)/admin/users/show/[id].tsx
index ff52de90..c5a89141 100644
--- a/src/app/(auth)/admin/users/show/[id].tsx
+++ b/src/app/(auth)/admin/users/show/[id].tsx
@@ -11,7 +11,7 @@ import UserAvatar from 'src/components/common/UserAvatar'
import { PressableOpacity } from 'react-native-pressable-opacity'
export default function Screen() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const router = useRouter()
const instance = Storage.getString('app.instance')
diff --git a/src/app/(auth)/chats/conversation/[id].tsx b/src/app/(auth)/chats/conversation/[id].tsx
index 701143d7..ca71c860 100644
--- a/src/app/(auth)/chats/conversation/[id].tsx
+++ b/src/app/(auth)/chats/conversation/[id].tsx
@@ -6,16 +6,13 @@ import { Stack, useLocalSearchParams, useNavigation, useRouter } from 'expo-rout
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { fetchChatThread, sendChatMessage, deleteChatMessage } from 'src/lib/api'
import { _timeAgo, enforceLen } from 'src/utils'
-import {
- GiftedChat,
- Bubble,
- Send,
- type BubbleProps,
- type IMessage,
-} from 'react-native-gifted-chat'
+import { GiftedChat, Bubble, Send } from 'react-native-gifted-chat'
+
import { Feather } from '@expo/vector-icons'
import { useUserCache } from 'src/state/AuthProvider'
+import type { BubbleProps, IMessage } from 'react-native-gifted-chat'
+
function renderBubble(props: BubbleProps) {
return (
(props: BubbleProps) {
}
export default function Page() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const router = useRouter()
const queryClient = useQueryClient()
const navigation = useNavigation()
@@ -45,20 +42,35 @@ export default function Page() {
const sendMutation = useMutation({
mutationFn: async (message) => {
- const res = await sendChatMessage(id, message[0].text)
- const msg = {
- _id: res.id,
- id: res.id,
- createdAt: new Date(),
- sent: true,
- text: message[0].text,
- user: {
- _id: selfUser.id,
- name: selfUser.username,
- avatar: selfUser.avatar,
- },
+ try {
+ const res = await sendChatMessage(id, message[0].text)
+ console.log('send message - server answered:', { res })
+
+ if (typeof res.error !== 'undefined') {
+ throw new Error(res.error)
+ }
+
+ const msg = {
+ _id: res.id,
+ id: res.id,
+ createdAt: new Date(),
+ sent: true,
+ text: message[0].text,
+ user: {
+ _id: selfUser.id,
+ name: selfUser.username,
+ avatar: selfUser.avatar,
+ },
+ }
+ setMessages((previousMessages) => GiftedChat.append(previousMessages, msg))
+ } catch (err: any) {
+ console.log('Failed to send message', err, err?.msg || err?.message)
+ Alert.alert('Failed to send message', err?.message || err)
}
- setMessages((previousMessages) => GiftedChat.append(previousMessages, msg))
+ },
+ onError: (err) => {
+ console.log('sendMutation: Failed to send message', err)
+ Alert.alert('Failed to send message', err.message)
},
})
diff --git a/src/app/(auth)/hashtag/[id].tsx b/src/app/(auth)/hashtag/[id].tsx
index 3843a567..a40e8d7c 100644
--- a/src/app/(auth)/hashtag/[id].tsx
+++ b/src/app/(auth)/hashtag/[id].tsx
@@ -54,7 +54,7 @@ const RenderItem = ({ item }) =>
) : null
export default function Page() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const queryClient = useQueryClient()
const RelatedTags = useCallback(
diff --git a/src/app/(auth)/post/[id].tsx b/src/app/(auth)/post/[id].tsx
index 242dbea8..18e76e8b 100644
--- a/src/app/(auth)/post/[id].tsx
+++ b/src/app/(auth)/post/[id].tsx
@@ -12,7 +12,7 @@ import CommentFeed from 'src/components/post/CommentFeed'
import { useUserCache } from 'src/state/AuthProvider'
export default function Page() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const navigation = useNavigation()
useLayoutEffect(() => {
@@ -83,7 +83,7 @@ export default function Page() {
const { isPending, isError, data, error } = useQuery({
queryKey: ['getStatusById', id],
- queryFn: getStatusById,
+ queryFn: () => getStatusById(id),
})
if (isPending) {
return (
diff --git a/src/app/(auth)/post/edit/[id].tsx b/src/app/(auth)/post/edit/[id].tsx
index 4e8fe54d..3ca50908 100644
--- a/src/app/(auth)/post/edit/[id].tsx
+++ b/src/app/(auth)/post/edit/[id].tsx
@@ -63,7 +63,7 @@ const RenderItem = React.memo(({ item, onUpdateMediaAlt }) => (
))
export default function Page() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const navigation = useNavigation()
const [caption, setCaption] = useState('')
const [isSensitive, setSensitive] = useState(false)
@@ -111,6 +111,8 @@ export default function Page() {
return m.description !== ogm.description
})
+ // TODO: error handling
+ // TODO: invalidate react query cache of this post so it is updated in the UI
await Promise.all(mediaChanges.map(updateMedia))
.then(async (res) => {
return await putEditPost(data?.id, {
diff --git a/src/app/(auth)/post/history/[id].tsx b/src/app/(auth)/post/history/[id].tsx
index 2f7b9129..618e589d 100644
--- a/src/app/(auth)/post/history/[id].tsx
+++ b/src/app/(auth)/post/history/[id].tsx
@@ -9,7 +9,7 @@ import { _timeAgo, htmlToTextWithLineBreaks } from 'src/utils'
import ReadMore from 'src/components/common/ReadMore'
export default function Page() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const navigation = useNavigation()
useLayoutEffect(() => {
diff --git a/src/app/(auth)/post/likes/[id].tsx b/src/app/(auth)/post/likes/[id].tsx
index d9a943e7..0e762474 100644
--- a/src/app/(auth)/post/likes/[id].tsx
+++ b/src/app/(auth)/post/likes/[id].tsx
@@ -7,7 +7,7 @@ import { getStatusById, getStatusLikes } from 'src/lib/api'
import UserAvatar from 'src/components/common/UserAvatar'
export default function Page() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const RenderItem = ({ item }) => {
return (
@@ -35,7 +35,7 @@ export default function Page() {
const { data: status } = useQuery({
queryKey: ['getStatusById', id],
- queryFn: getStatusById,
+ queryFn: () => getStatusById(id),
})
const statusId = status?.id
diff --git a/src/app/(auth)/post/report/[id].tsx b/src/app/(auth)/post/report/[id].tsx
index ae8fd181..0077709d 100644
--- a/src/app/(auth)/post/report/[id].tsx
+++ b/src/app/(auth)/post/report/[id].tsx
@@ -3,26 +3,18 @@ import { SafeAreaView } from 'react-native-safe-area-context'
import { ScrollView, Text, XStack, YStack } from 'tamagui'
import { Feather } from '@expo/vector-icons'
import { useMutation } from '@tanstack/react-query'
-import { reportStatus } from 'src/lib/api'
-import { ActivityIndicator, Pressable } from 'react-native'
+import { report } from 'src/lib/api'
+import { ActivityIndicator, Alert, Pressable } from 'react-native'
+import { reportTypes } from 'src/lib/reportTypes'
+
+import type { NewReport } from 'src/lib/api'
+import type { ReportType } from 'src/lib/reportTypes'
export default function Page() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const router = useRouter()
- const reportTypes = [
- { name: 'spam', title: "It's spam" },
- { name: 'sensitive', title: 'Nudity or sexual activity' },
- { name: 'abusive', title: 'Bullying or harassment' },
- { name: 'underage', title: 'I think this account is underage' },
- { name: 'violence', title: 'Violence or dangerous organizations' },
- { name: 'copyright', title: 'Copyright infringement' },
- { name: 'impersonation', title: 'Impersonation' },
- { name: 'scam', title: 'Scam or fraud' },
- { name: 'terrorism', title: 'Terrorism or terrorism-related content' },
- ]
-
- const RenderOption = ({ title, name }) => (
+ const RenderOption = ({ title, name }: ReportType) => (
handleAction(name)}>
)
- const handleAction = (type) => {
- mutation.mutate({ id: id, type: type })
+ const handleAction = (type: string) => {
+ mutation.mutate({ object_id: id, object_type: 'post', report_type: type })
}
const mutation = useMutation({
- mutationFn: (newReport) => {
- return reportStatus(newReport)
+ mutationFn: (newReport: NewReport) => {
+ return report(newReport)
},
onSuccess: (data, variables, context) => {
router.replace('/post/report/sent')
},
+ onError: (err) => {
+ Alert.alert('Report Failed', err.message)
+ },
})
return (
diff --git a/src/app/(auth)/post/shares/[id].tsx b/src/app/(auth)/post/shares/[id].tsx
index a77adcac..88332360 100644
--- a/src/app/(auth)/post/shares/[id].tsx
+++ b/src/app/(auth)/post/shares/[id].tsx
@@ -7,7 +7,7 @@ import { getStatusById, getStatusReblogs } from 'src/lib/api'
import UserAvatar from 'src/components/common/UserAvatar'
export default function Page() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const RenderItem = ({ item }) => {
return (
@@ -35,7 +35,7 @@ export default function Page() {
const { data: status } = useQuery({
queryKey: ['getStatusById', id],
- queryFn: getStatusById,
+ queryFn: () => getStatusById(id),
})
const statusId = status?.id
diff --git a/src/app/(auth)/profile/[id].tsx b/src/app/(auth)/profile/[id].tsx
index 62985848..3bafee1c 100644
--- a/src/app/(auth)/profile/[id].tsx
+++ b/src/app/(auth)/profile/[id].tsx
@@ -48,7 +48,7 @@ const SCREEN_WIDTH = Dimensions.get('screen').width
export default function ProfileScreen() {
const navigation = useNavigation()
- const { id, byUsername } = useLocalSearchParams()
+ const { id, byUsername } = useLocalSearchParams<{ id: string; byUsername?: string }>()
const queryClient = useQueryClient()
const bottomSheetModalRef = useRef(null)
const snapPoints = useMemo(() => ['50%', '55%'], [])
@@ -332,7 +332,7 @@ export default function ProfileScreen() {
bottomSheetModalRef.current?.close()
if (action === 'report') {
- router.push('/profile/report/' + id)
+ router.push('/profile/report/' + userId)
}
if (action === 'block') {
@@ -347,7 +347,7 @@ export default function ProfileScreen() {
{
text: 'Block',
style: 'destructive',
- onPress: () => _handleBlock(),
+ onPress: () => handleBlock(),
},
]
)
@@ -362,7 +362,7 @@ export default function ProfileScreen() {
{
text: 'Unblock',
style: 'destructive',
- onPress: () => _handleUnblock(),
+ onPress: () => handleUnblock(),
},
])
}
@@ -379,7 +379,7 @@ export default function ProfileScreen() {
{
text: 'Mute',
style: 'destructive',
- onPress: () => _handleMute(),
+ onPress: () => handleMute(),
},
]
)
@@ -394,7 +394,7 @@ export default function ProfileScreen() {
{
text: 'Unmute',
style: 'destructive',
- onPress: () => _handleUnmute(),
+ onPress: () => handleUnmute(),
},
])
}
@@ -426,35 +426,35 @@ export default function ProfileScreen() {
}
}
- const _handleBlock = () => {
+ const handleBlock = () => {
blockMutation.mutate()
}
- const _handleUnblock = () => {
+ const handleUnblock = () => {
unblockMutation.mutate()
}
- const _handleMute = () => {
+ const handleMute = () => {
muteMutation.mutate()
}
- const _handleUnmute = () => {
+ const handleUnmute = () => {
unmuteMutation.mutate()
}
- const _handleFollow = () => {
+ const handleFollow = () => {
followMutation.mutate()
}
- const _handleUnfollow = () => {
+ const handleUnfollow = () => {
unfollowMutation.mutate()
}
- const _handleCancelFollowRequest = () => {
+ const handleCancelFollowRequest = () => {
unfollowMutation.mutate()
}
- const _handleOnShare = async () => {
+ const handleOnShare = async () => {
try {
const result = await Share.share({
message: user.url,
@@ -466,13 +466,19 @@ export default function ProfileScreen() {
}
}
- const { data: user, error: userError } = useQuery({
- queryKey:
- byUsername !== undefined && id == 0
- ? ['getAccountByUsername', byUsername]
- : ['getAccountById', id],
- queryFn: byUsername !== undefined && id == 0 ? getAccountByUsername : getAccountById,
- })
+ const { data: user, error: userError } = useQuery(
+ byUsername !== undefined && id === '0'
+ ? {
+ queryKey: ['getAccountByUsername', byUsername],
+ queryFn: () => getAccountByUsername(byUsername),
+ }
+ : {
+ queryKey: ['getAccountById', id],
+ queryFn: () => getAccountById(id),
+ }
+ )
+
+ const userId = user?.id
useEffect(() => {
if (user && Platform.OS == 'android') {
@@ -487,8 +493,6 @@ export default function ProfileScreen() {
}
}, [navigation, user])
- const userId = user?.id
-
const { data: relationship, isError: relationshipError } = useQuery({
queryKey: ['getAccountRelationship', userId],
queryFn: getAccountRelationship,
@@ -507,10 +511,10 @@ export default function ProfileScreen() {
profile={user}
relationship={relationship}
openMenu={onOpenMenu}
- onFollow={() => _handleFollow()}
- onUnfollow={() => _handleUnfollow()}
- onCancelFollowRequest={() => _handleCancelFollowRequest()}
- onShare={() => _handleOnShare()}
+ onFollow={() => handleFollow()}
+ onUnfollow={() => handleUnfollow()}
+ onCancelFollowRequest={() => handleCancelFollowRequest()}
+ onShare={() => handleOnShare()}
mutuals={mutuals}
/>
),
@@ -557,6 +561,34 @@ export default function ProfileScreen() {
enabled: !!userId,
})
+ if (userError) {
+ return (
+
+
+
+ {userError.message}
+ {byUsername && (
+
+ )}
+
+
+ )
+ }
+
if (status !== 'success' || (isFetching && !isFetchingNextPage)) {
return (
diff --git a/src/app/(auth)/profile/about/[id].tsx b/src/app/(auth)/profile/about/[id].tsx
index 59b77e6a..d967e0fa 100644
--- a/src/app/(auth)/profile/about/[id].tsx
+++ b/src/app/(auth)/profile/about/[id].tsx
@@ -9,7 +9,7 @@ import { getAccountById, getAccountRelationship } from 'src/lib/api'
import { formatTimestampMonthYear, formatTimestamp, getDomain } from 'src/utils'
export default function ProfileScreen() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const router = useRouter()
const {
@@ -18,7 +18,7 @@ export default function ProfileScreen() {
isFetching: isFetchingUser,
} = useQuery({
queryKey: ['getAccountById', id],
- queryFn: getAccountById,
+ queryFn: () => getAccountById(id),
})
if (userError) {
diff --git a/src/app/(auth)/profile/followers/[id].tsx b/src/app/(auth)/profile/followers/[id].tsx
index 1d2243e5..dd23f040 100644
--- a/src/app/(auth)/profile/followers/[id].tsx
+++ b/src/app/(auth)/profile/followers/[id].tsx
@@ -9,7 +9,7 @@ import UserAvatar from 'src/components/common/UserAvatar'
import Feather from '@expo/vector-icons/Feather'
export default function FollowersScreen() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const navigation = useNavigation()
useLayoutEffect(() => {
navigation.setOptions({ title: 'Followers', headerBackTitle: 'Back' })
@@ -36,7 +36,7 @@ export default function FollowersScreen() {
const { data: profile } = useQuery({
queryKey: ['getAccountById', id],
- queryFn: getAccountById,
+ queryFn: () => getAccountById(id),
})
const profileId = profile?.id
diff --git a/src/app/(auth)/profile/following/[id].tsx b/src/app/(auth)/profile/following/[id].tsx
index d7fa2927..aac90a84 100644
--- a/src/app/(auth)/profile/following/[id].tsx
+++ b/src/app/(auth)/profile/following/[id].tsx
@@ -9,7 +9,7 @@ import UserAvatar from 'src/components/common/UserAvatar'
import Feather from '@expo/vector-icons/Feather'
export default function FollowingScreen() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const navigation = useNavigation()
useLayoutEffect(() => {
navigation.setOptions({ title: 'Following', headerBackTitle: 'Back' })
@@ -36,7 +36,7 @@ export default function FollowingScreen() {
const { data: profile } = useQuery({
queryKey: ['getAccountById', id],
- queryFn: getAccountById,
+ queryFn: () => getAccountById(id),
})
const profileId = profile?.id
diff --git a/src/app/(auth)/profile/report/[id].tsx b/src/app/(auth)/profile/report/[id].tsx
index fc71bfa9..a2648d50 100644
--- a/src/app/(auth)/profile/report/[id].tsx
+++ b/src/app/(auth)/profile/report/[id].tsx
@@ -3,26 +3,18 @@ import { SafeAreaView } from 'react-native-safe-area-context'
import { ScrollView, Text, View, XStack, YStack } from 'tamagui'
import { Feather } from '@expo/vector-icons'
import { useMutation } from '@tanstack/react-query'
-import { reportProfile } from 'src/lib/api'
-import { ActivityIndicator, Pressable } from 'react-native'
+import { report } from 'src/lib/api'
+import { ActivityIndicator, Alert, Pressable } from 'react-native'
+import { reportTypes } from 'src/lib/reportTypes'
+
+import type { NewReport } from 'src/lib/api'
+import type { ReportType } from 'src/lib/reportTypes'
export default function Page() {
- const { id } = useLocalSearchParams()
+ const { id } = useLocalSearchParams<{ id: string }>()
const router = useRouter()
- const reportTypes = [
- { name: 'spam', title: "It's spam" },
- { name: 'sensitive', title: 'Nudity or sexual activity' },
- { name: 'abusive', title: 'Bullying or harassment' },
- { name: 'underage', title: 'I think this account is underage' },
- { name: 'violence', title: 'Violence or dangerous organizations' },
- { name: 'copyright', title: 'Copyright infringement' },
- { name: 'impersonation', title: 'Impersonation' },
- { name: 'scam', title: 'Scam or fraud' },
- { name: 'terrorism', title: 'Terrorism or terrorism-related content' },
- ]
-
- const RenderOption = ({ title, name }) => (
+ const RenderOption = ({ title, name }: ReportType) => (
handleAction(name)}>
)
- const handleAction = (type) => {
- mutation.mutate({ id: id, type: type })
+ const handleAction = (type: string) => {
+ mutation.mutate({ object_id: id, object_type: 'user', report_type: type })
}
const mutation = useMutation({
- mutationFn: (newReport) => {
- return reportProfile(newReport)
+ mutationFn: (newReport: NewReport) => {
+ return report(newReport)
},
onSuccess: (data, variables, context) => {
router.replace('/profile/report/sent')
},
+ onError: (err) => {
+ Alert.alert('Report Failed', err.message)
+ },
})
return (
diff --git a/src/app/(auth)/search/index.tsx b/src/app/(auth)/search/index.tsx
index 321406c1..93783929 100644
--- a/src/app/(auth)/search/index.tsx
+++ b/src/app/(auth)/search/index.tsx
@@ -1,4 +1,4 @@
-import { Link, Stack } from 'expo-router'
+import { Link, Stack, useLocalSearchParams } from 'expo-router'
import { FlatList, ActivityIndicator, Keyboard, Pressable } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Text, View, YStack, Input, XStack } from 'tamagui'
@@ -13,7 +13,9 @@ import ReadMore from '../../../components/common/ReadMore'
import { formatTimestampMonthYear, postCountLabel } from '../../../utils'
export default function SearchScreen() {
- const [query, setQuery] = useState('')
+ const { initialQuery } = useLocalSearchParams<{ initialQuery?: string }>()
+
+ const [query, setQuery] = useState(initialQuery || '')
const { data, isLoading, isError, error, isFetching } = useQuery({
queryKey: ['search', query],
diff --git a/src/lib/api-context.ts b/src/lib/api-context.ts
new file mode 100644
index 00000000..48807248
--- /dev/null
+++ b/src/lib/api-context.ts
@@ -0,0 +1,118 @@
+import { Storage } from 'src/state/cache'
+import { randomKey } from './randomKey'
+
+type ApiRequestOptions = {
+ idempotency?: true
+ searchParams?: Record
+}
+
+export class ApiContext {
+ constructor(
+ public readonly instanceHostName: string,
+ public readonly token: string
+ ) {
+ if (instanceHostName.length === 0) {
+ throw new Error('failed to create ApiContext: instance host name missing')
+ }
+ if (token.length === 0) {
+ throw new Error('failed to create ApiContext: token missing')
+ }
+ }
+
+ // TODO: private
+ async request(
+ path: string,
+ fetch_options: RequestInit,
+ options?: ApiRequestOptions
+ ): Promise {
+ const url = new URL(`${path}`, `https://${this.instanceHostName}`)
+
+ if (options?.searchParams) {
+ let { searchParams } = options
+ for (const key in searchParams) {
+ if (Object.prototype.hasOwnProperty.call(searchParams, key)) {
+ url.searchParams.append(key, String(searchParams[key]))
+ }
+ }
+ }
+
+ fetch_options.headers = {
+ ...fetch_options.headers,
+ Accept: 'application/json',
+ Authorization: `Bearer ${this.token}`,
+ ...(options?.idempotency ? { 'Idempotency-Key': randomKey(40) } : {}),
+ }
+
+ let response = await fetch(url, fetch_options)
+ if (!response.ok) {
+ let errorResponse
+ try {
+ errorResponse = await response.json()
+ console.warn('API Request Failed', { errorResponse })
+ if (
+ !errorResponse.error_code &&
+ (typeof errorResponse.error === 'undefined' || errorResponse.error.length === 0)
+ ) {
+ throw new Error('request failed, but error field is empty')
+ }
+ } catch (error) {
+ console.error('API Request Failed - Failed to decode error:', error)
+ throw new Error('API Request Failed without error message')
+ }
+ if (errorResponse.error_code) {
+ throw new Error(
+ `API Request Failed: ${errorResponse.error_code}: ${errorResponse.msg}`
+ )
+ }
+ throw new Error(`API Request Failed: ${errorResponse.error}`)
+ }
+
+ return response
+ }
+
+ /** request in json data and gets back json data */
+ async jsonRequest(
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
+ path: string,
+ data = {},
+ searchParams: ApiRequestOptions['searchParams'] = {}
+ ) {
+ return await (
+ await this.request(
+ path,
+ {
+ method,
+ body: JSON.stringify(data),
+ headers: { 'Content-Type': 'application/json' },
+ },
+ { searchParams }
+ )
+ ).json()
+ }
+
+ async get(url: string, searchParams: ApiRequestOptions['searchParams'] = {}) {
+ const response = await this.request(
+ url,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ { searchParams }
+ )
+ return await response.json()
+ }
+}
+
+export function ContextFromStorage(): ApiContext {
+ const instance = Storage.getString('app.instance')
+ const token = Storage.getString('app.token')
+ if (!instance || !token) {
+ throw new Error('api token or instance undefined')
+ }
+ return new ApiContext(instance, token)
+}
+
+// For debugging
+;(global as any).API = ContextFromStorage
diff --git a/src/lib/api-types.d.ts b/src/lib/api-types.d.ts
index e463f9a6..81ee9b7a 100644
--- a/src/lib/api-types.d.ts
+++ b/src/lib/api-types.d.ts
@@ -100,16 +100,29 @@ export type Relationship = {
blocking: boolean
domain_blocking
endorsed: boolean
- followed_by?
- following
+ followed_by?: boolean
+ following: boolean
following_since
id
- muting
+ muting: boolean
muting_notifications
- requested
+ requested: boolean
showing_reblogs
}
+export type RelationshipFromFollowAPIResponse = {
+ blocking: boolean
+ domain_blocking: any | null
+ endorsed: boolean
+ followed_by?: boolean
+ following: boolean
+ id: string
+ muting: boolean
+ muting_notifications: any | null
+ requested: boolean
+ showing_reblogs: any | null
+}
+
export type Account = {
/** value is username */
acct: string
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 1011f766..424b9cda 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -1,7 +1,16 @@
import { objectToForm } from 'src/requests'
import { Storage } from 'src/state/cache'
import { parseLinkHeader } from 'src/utils'
-import type { PaginatedStatus, Relationship, UpdateCredentialsParams } from './api-types'
+import type {
+ Account,
+ PaginatedStatus,
+ Relationship,
+ RelationshipFromFollowAPIResponse,
+ UpdateCredentialsParams,
+ Status,
+} from './api-types'
+import { randomKey } from './randomKey'
+import { ContextFromStorage } from './api-context'
export function randomKey(length: number) {
let result = ''
@@ -59,35 +68,6 @@ export async function selfPost(
return rawRes ? resp : resp.json()
}
-export async function selfPut(
- path: string,
- params = {},
- asForm = false,
- rawRes = false,
- idempotency = false
-) {
- let headers: Record = {}
- const instance = Storage.getString('app.instance')
- const token = Storage.getString('app.token')
- const url = `https://${instance}/${path}`
-
- headers['Authorization'] = `Bearer ${token}`
- headers['Accept'] = 'application/json'
- headers['Content-Type'] = asForm ? 'multipart/form-data' : 'application/json'
-
- if (idempotency) {
- headers['Idempotency-Key'] = randomKey(40)
- }
-
- const resp = await fetch(url, {
- method: 'PUT',
- body: asForm ? objectToForm(params) : JSON.stringify(params),
- headers,
- })
-
- return rawRes ? resp : resp.json()
-}
-
export async function selfDelete(
path: string,
params = {},
@@ -183,6 +163,8 @@ async function fetchCursorPagination(url: string) {
}
async function fetchData(url: string) {
+ console.log('deprected fetchData called', url)
+
const token = Storage.getString('app.token')
const response = await fetch(url, {
@@ -294,59 +276,72 @@ export async function getAccountFollowing(id: string, cursor) {
return await fetchPaginatedData(url)
}
-export async function getStatusById({ queryKey }) {
- const instance = Storage.getString('app.instance')
- const url = `https://${instance}/api/v1/statuses/${queryKey[1]}?_pe=1`
- return await fetchData(url)
+export async function getStatusById(id: string) {
+ const api = ContextFromStorage()
+ return await api.get(`api/v1/statuses/${id}?_pe=1`)
}
-export async function getAccountById({ queryKey }) {
- const instance = Storage.getString('app.instance')
- const url = `https://${instance}/api/v1/accounts/${queryKey[1]}?_pe=1`
- return await fetchData(url)
+export async function getAccountById(id: string) {
+ const api = ContextFromStorage()
+ return await api.get(`api/v1/accounts/${id}?_pe=1`)
}
-export async function followAccountById(id: string) {
- let path = `api/v1/accounts/${id}/follow`
- return await selfPost(path)
+export async function followAccountById(
+ id: string
+): Promise {
+ const api = ContextFromStorage()
+ return await api.jsonRequest('POST', `api/v1/accounts/${id}/follow`)
}
-export async function unfollowAccountById(id: string) {
- let path = `api/v1/accounts/${id}/unfollow`
- return await selfPost(path)
+export async function unfollowAccountById(
+ id: string
+): Promise {
+ const api = ContextFromStorage()
+ return await api.jsonRequest('POST', `api/v1/accounts/${id}/unfollow`)
}
-export async function reportProfile({ id, type }) {
- const instance = Storage.getString('app.instance')
- const token = Storage.getString('app.token')
-
- const params = new URLSearchParams({
- report_type: type,
- object_type: 'user',
- object_id: id,
- })
- const url = `https://${instance}/api/v1.1/report?${params}`
- const response = await fetch(url, {
- method: 'post',
- headers: new Headers({
- Authorization: `Bearer ${token}`,
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- }),
- })
- return await response.json()
+export type NewReport = {
+ object_id: string
+ report_type: string
+ object_type: 'user' | 'post'
}
-export async function getAccountByUsername({ queryKey }) {
- const instance = Storage.getString('app.instance')
- const url = `https://${instance}/api/v1.1/accounts/username/${queryKey[1]}?_pe=1`
- return await fetchData(url)
+export async function report(report: NewReport) {
+ const api = ContextFromStorage()
+ const response = await api.jsonRequest('POST', 'api/v1.1/report', {}, report)
+ return await response.json()
}
-export async function getAccountStatusesById(id: string, page) {
- const instance = Storage.getString('app.instance')
- const url = `https://${instance}/api/v1/accounts/${id}/statuses?_pe=1&limit=24&max_id=${page}`
- return await fetchData(url)
+export async function getAccountByUsername(username: string): Promise {
+ const api = ContextFromStorage()
+ let account = await api.get(`api/v1.1/accounts/username/${username}?_pe=1`)
+ if (Array.isArray(account) && account.length === 0) {
+ throw new Error(`Account "${username}" not found`)
+ }
+ return account
+}
+
+interface getAccountStatusesByIdParameters {
+ // https://github.com/pixelfed/pixelfed/blob/fa4474bc38d64b1d96272f9d45e90289020fcb11/app/Http/Controllers/Api/ApiV1Controller.php#L699
+ only_media?: true
+ pinned?: true
+ exclude_replies?: true
+ media_type?: 'photo' | 'video'
+ limit?: number
+ max_id?: number
+ since_id?: number
+ min_id?: number
+}
+
+export async function getAccountStatusesById(
+ id: string,
+ parameters: getAccountStatusesByIdParameters
+): Promise {
+ const api = ContextFromStorage()
+ return await api.get(`api/v1/accounts/${id}/statuses`, {
+ _pe: 1, // todo document what _pe means
+ ...parameters,
+ })
}
export async function getHashtagByName({ queryKey }) {
@@ -525,27 +520,6 @@ export async function unreblogStatus({ id }: { id: string }) {
return await selfPost(`api/v1/statuses/${id}/unreblog`)
}
-export async function reportStatus({ id, type }) {
- const instance = Storage.getString('app.instance')
- const token = Storage.getString('app.token')
-
- const params = new URLSearchParams({
- report_type: type,
- object_type: 'post',
- object_id: id,
- })
- const url = `https://${instance}/api/v1.1/report?${params}`
- const response = await fetch(url, {
- method: 'post',
- headers: new Headers({
- Authorization: `Bearer ${token}`,
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- }),
- })
- return await response.json()
-}
-
export async function deleteStatus({ id }: { id: string }) {
const instance = Storage.getString('app.instance')
const token = Storage.getString('app.token')
@@ -625,9 +599,10 @@ export async function getFollowRequests() {
}
export async function getSelfAccount() {
- const instance = Storage.getString('app.instance')
- let url = `https://${instance}/api/v1/accounts/verify_credentials?_pe=1`
- return await fetchData(url)
+ const api = ContextFromStorage()
+ return await api.get('api/v1/accounts/verify_credentials', {
+ _pe: 1, // todo document what _pe means
+ })
}
export async function updateCredentials(params: URLSearchParams) {
@@ -693,7 +668,7 @@ export async function deleteChatMessage(id: string) {
return await selfDelete(path)
}
-export async function sendChatMessage(id: string, message) {
+export async function sendChatMessage(id: string, message: string) {
const path = `api/v1.1/direct/thread/send`
return await selfPost(path, {
to_id: id,
@@ -861,7 +836,8 @@ export async function getSelfBookmarks({ pageParam = false }) {
}
export async function putEditPost(id: string, params) {
- return await selfPut(`api/v1/statuses/${id}`, params)
+ let api = ContextFromStorage()
+ return await api.jsonRequest('PUT', `api/v1/statuses/${id}`, params)
}
export async function getStoryCarousel() {
diff --git a/src/lib/randomKey.ts b/src/lib/randomKey.ts
new file mode 100644
index 00000000..161e9433
--- /dev/null
+++ b/src/lib/randomKey.ts
@@ -0,0 +1,11 @@
+export function randomKey(length: number) {
+ let result = ''
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+ const charactersLength = characters.length
+ let counter = 0
+ while (counter < length) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength))
+ counter += 1
+ }
+ return result
+}
diff --git a/src/lib/reportTypes.ts b/src/lib/reportTypes.ts
new file mode 100644
index 00000000..b4b573f0
--- /dev/null
+++ b/src/lib/reportTypes.ts
@@ -0,0 +1,13 @@
+export type ReportType = { name: string; title: string }
+
+export const reportTypes: ReportType[] = [
+ { name: 'spam', title: "It's spam" },
+ { name: 'sensitive', title: 'Nudity or sexual activity' },
+ { name: 'abusive', title: 'Bullying or harassment' },
+ { name: 'underage', title: 'I think this account is underage' },
+ { name: 'violence', title: 'Violence or dangerous organizations' },
+ { name: 'copyright', title: 'Copyright infringement' },
+ { name: 'impersonation', title: 'Impersonation' },
+ { name: 'scam', title: 'Scam or fraud' },
+ { name: 'terrorism', title: 'Terrorism or terrorism-related content' },
+]
diff --git a/src/state/AuthProvider.tsx b/src/state/AuthProvider.tsx
index a6dd1602..6c0c19ea 100644
--- a/src/state/AuthProvider.tsx
+++ b/src/state/AuthProvider.tsx
@@ -267,7 +267,7 @@ export function useQuerySelfProfile() {
isFetchedAfterMount,
} = useQuery({
queryKey: ['profileById', userCache.id],
- queryFn: getAccountById,
+ queryFn: () => getAccountById(userCache.id),
placeholderData: userCache,
})