mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Merge branch 'main' into candidate
This commit is contained in:
@ -1,2 +1 @@
|
|||||||
Enjoy toooting! This version includes following improvements and fixes:
|
Enjoy toooting! This version includes following improvements and fixes:
|
||||||
- Added Belarusian language
|
|
@ -1,2 +1 @@
|
|||||||
toooting愉快!此版本包括以下改进和修复:
|
toooting愉快!此版本包括以下改进和修复:
|
||||||
- 新增白俄罗斯语
|
|
@ -1,9 +1,7 @@
|
|||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <RCTAppDelegate.h>
|
#import <RCTAppDelegate.h>
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
#import <Expo/Expo.h>
|
#import <Expo/Expo.h>
|
||||||
|
|
||||||
@interface AppDelegate : RCTAppDelegate
|
@interface AppDelegate : EXAppDelegateWrapper
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -51,4 +51,22 @@
|
|||||||
restorationHandler:restorationHandler];
|
restorationHandler:restorationHandler];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
|
||||||
|
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
|
||||||
|
{
|
||||||
|
return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
|
||||||
|
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
|
||||||
|
{
|
||||||
|
return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
|
||||||
|
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
|
||||||
|
{
|
||||||
|
return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tooot",
|
"name": "tooot",
|
||||||
"version": "4.9.1",
|
"version": "4.9.2",
|
||||||
"description": "tooot for Mastodon",
|
"description": "tooot for Mastodon",
|
||||||
"author": "xmflsct <me@xmflsct.com>",
|
"author": "xmflsct <me@xmflsct.com>",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
@ -88,7 +88,6 @@
|
|||||||
"react-native-swipe-list-view": "^3.2.9",
|
"react-native-swipe-list-view": "^3.2.9",
|
||||||
"react-native-tab-view": "^3.5.1",
|
"react-native-tab-view": "^3.5.1",
|
||||||
"rn-placeholder": "^3.0.3",
|
"rn-placeholder": "^3.0.3",
|
||||||
"url-parse": "^1.5.10",
|
|
||||||
"zeego": "^1.3.1"
|
"zeego": "^1.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -102,7 +101,6 @@
|
|||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/react-native-share-menu": "^5.0.2",
|
"@types/react-native-share-menu": "^5.0.2",
|
||||||
"@types/url-parse": "^1.4.8",
|
|
||||||
"babel-plugin-module-resolver": "^5.0.0",
|
"babel-plugin-module-resolver": "^5.0.0",
|
||||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
|
@ -21,6 +21,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||||||
import * as AuthSession from 'expo-auth-session'
|
import * as AuthSession from 'expo-auth-session'
|
||||||
import * as Crypto from 'expo-crypto'
|
import * as Crypto from 'expo-crypto'
|
||||||
import { Image } from 'expo-image'
|
import { Image } from 'expo-image'
|
||||||
|
import * as Linking from 'expo-linking'
|
||||||
import * as WebBrowser from 'expo-web-browser'
|
import * as WebBrowser from 'expo-web-browser'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import React, { RefObject, useCallback, useState } from 'react'
|
import React, { RefObject, useCallback, useState } from 'react'
|
||||||
@ -28,7 +29,6 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||||||
import { Alert, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
|
import { Alert, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
|
||||||
import { ScrollView } from 'react-native-gesture-handler'
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
import { fromByteArray } from 'react-native-quick-base64'
|
import { fromByteArray } from 'react-native-quick-base64'
|
||||||
import parse from 'url-parse'
|
|
||||||
import CustomText from '../Text'
|
import CustomText from '../Text'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -51,7 +51,7 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
const whitelisted: boolean =
|
const whitelisted: boolean =
|
||||||
!!domain.length &&
|
!!domain.length &&
|
||||||
!!errorCode &&
|
!!errorCode &&
|
||||||
!!(parse(`https://${domain}/`).hostname === domain) &&
|
!!(Linking.parse(`https://${domain}/`).hostname === domain) &&
|
||||||
errorCode === 401
|
errorCode === 401
|
||||||
|
|
||||||
const instanceQuery = useInstanceQuery({
|
const instanceQuery = useInstanceQuery({
|
||||||
@ -129,7 +129,7 @@ const ComponentInstance: React.FC<Props> = ({
|
|||||||
(instanceQuery.data as Mastodon.Instance_V2)?.domain ||
|
(instanceQuery.data as Mastodon.Instance_V2)?.domain ||
|
||||||
instanceQuery.data?.account_domain ||
|
instanceQuery.data?.account_domain ||
|
||||||
((instanceQuery.data as Mastodon.Instance_V1)?.uri
|
((instanceQuery.data as Mastodon.Instance_V1)?.uri
|
||||||
? parse((instanceQuery.data as Mastodon.Instance_V1).uri).hostname
|
? Linking.parse((instanceQuery.data as Mastodon.Instance_V1).uri).hostname
|
||||||
: undefined) ||
|
: undefined) ||
|
||||||
(instanceQuery.data as Mastodon.Instance_V1)?.uri,
|
(instanceQuery.data as Mastodon.Instance_V1)?.uri,
|
||||||
'auth.account.avatar_static': avatar_static,
|
'auth.account.avatar_static': avatar_static,
|
||||||
|
125
src/components/Timeline/Shared/Card/Neodb.tsx
Normal file
125
src/components/Timeline/Shared/Card/Neodb.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import GracefullyImage from '@components/GracefullyImage'
|
||||||
|
import openLink from '@components/openLink'
|
||||||
|
import CustomText from '@components/Text'
|
||||||
|
import { useNeodbQuery } from '@utils/queryHooks/neodb'
|
||||||
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import * as Linking from 'expo-linking'
|
||||||
|
import { Pressable, View } from 'react-native'
|
||||||
|
|
||||||
|
export const CardNeodb = ({ card }: { card: Mastodon.Card }) => {
|
||||||
|
const { colors } = useTheme()
|
||||||
|
|
||||||
|
const segments = Linking.parse(card.url).path?.split('/')
|
||||||
|
if (!segments || !(segments[0] === 'movie' || segments[0] === 'book')) return null
|
||||||
|
|
||||||
|
const { data } = useNeodbQuery({ path: `${segments[0]}/${segments[1]}` })
|
||||||
|
|
||||||
|
if (!data) return null
|
||||||
|
|
||||||
|
switch (segments[0]) {
|
||||||
|
case 'movie':
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
style={{
|
||||||
|
marginTop: StyleConstants.Spacing.S,
|
||||||
|
backgroundColor: colors.shimmerDefault,
|
||||||
|
borderRadius: StyleConstants.BorderRadius,
|
||||||
|
padding: StyleConstants.Spacing.S,
|
||||||
|
flexDirection: 'row'
|
||||||
|
}}
|
||||||
|
onPress={() => openLink(card.url)}
|
||||||
|
>
|
||||||
|
<GracefullyImage
|
||||||
|
sources={{ default: { uri: `https://neodb.social${data.image}` } }}
|
||||||
|
dimension={{
|
||||||
|
width: StyleConstants.Font.LineHeight.M * 4,
|
||||||
|
height: StyleConstants.Font.LineHeight.M * 5
|
||||||
|
}}
|
||||||
|
style={{ marginRight: StyleConstants.Spacing.S }}
|
||||||
|
imageStyle={{ borderRadius: StyleConstants.BorderRadius / 2 }}
|
||||||
|
dim
|
||||||
|
/>
|
||||||
|
<View style={{ flex: 1, gap: StyleConstants.Spacing.S }}>
|
||||||
|
<CustomText
|
||||||
|
fontStyle='S'
|
||||||
|
fontWeight='Bold'
|
||||||
|
style={{ color: colors.primaryDefault }}
|
||||||
|
numberOfLines={3}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
data.data.title,
|
||||||
|
data.data.orig_title,
|
||||||
|
data.data.year ? `(${data.data.year})` : null
|
||||||
|
]
|
||||||
|
.filter(d => d)
|
||||||
|
.join(' ')}
|
||||||
|
</CustomText>
|
||||||
|
<CustomText fontStyle='S' style={{ color: colors.secondary }} numberOfLines={2}>
|
||||||
|
{[
|
||||||
|
data.data.duration ? `${data.data.duration}分钟` : null,
|
||||||
|
data.data.area?.join(' '),
|
||||||
|
data.data.genre?.join(' '),
|
||||||
|
data.data.director?.join(' ')
|
||||||
|
]
|
||||||
|
.filter(d => d)
|
||||||
|
.join(' / ')}
|
||||||
|
</CustomText>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
case 'book':
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
style={{
|
||||||
|
marginTop: StyleConstants.Spacing.S,
|
||||||
|
backgroundColor: colors.shimmerDefault,
|
||||||
|
borderRadius: StyleConstants.BorderRadius,
|
||||||
|
padding: StyleConstants.Spacing.S,
|
||||||
|
flexDirection: 'row'
|
||||||
|
}}
|
||||||
|
onPress={() => openLink(card.url)}
|
||||||
|
>
|
||||||
|
<GracefullyImage
|
||||||
|
sources={{ default: { uri: `https://neodb.social${data.image}` } }}
|
||||||
|
dimension={{
|
||||||
|
width: StyleConstants.Font.LineHeight.M * 4,
|
||||||
|
height: StyleConstants.Font.LineHeight.M * 5
|
||||||
|
}}
|
||||||
|
style={{ marginRight: StyleConstants.Spacing.S }}
|
||||||
|
imageStyle={{ borderRadius: StyleConstants.BorderRadius / 2 }}
|
||||||
|
dim
|
||||||
|
/>
|
||||||
|
<View style={{ flex: 1, gap: StyleConstants.Spacing.S }}>
|
||||||
|
<CustomText
|
||||||
|
fontStyle='S'
|
||||||
|
fontWeight='Bold'
|
||||||
|
style={{ color: colors.primaryDefault }}
|
||||||
|
numberOfLines={3}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
data.data.title,
|
||||||
|
data.data.pub_year && data.data.pub_month
|
||||||
|
? `(${data.data.pub_year}年${data.data.pub_month}月)`
|
||||||
|
: null
|
||||||
|
]
|
||||||
|
.filter(d => d)
|
||||||
|
.join(' ')}
|
||||||
|
</CustomText>
|
||||||
|
<CustomText fontStyle='S' style={{ color: colors.secondary }} numberOfLines={2}>
|
||||||
|
{[
|
||||||
|
data.data.author?.join(' '),
|
||||||
|
data.data.language,
|
||||||
|
data.data.pages ? `${data.data.pages}页` : null,
|
||||||
|
data.data.pub_house
|
||||||
|
]
|
||||||
|
.filter(d => d)
|
||||||
|
.join(' / ')}
|
||||||
|
</CustomText>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@ -11,14 +11,21 @@ import { useStatusQuery } from '@utils/queryHooks/status'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import TimelineDefault from '../Default'
|
import TimelineDefault from '../../Default'
|
||||||
import StatusContext from './Context'
|
import StatusContext from '../Context'
|
||||||
|
import { CardNeodb } from './Neodb'
|
||||||
|
|
||||||
const TimelineCard: React.FC = () => {
|
const TimelineCard: React.FC = () => {
|
||||||
const { status, spoilerHidden, disableDetails, inThread } = useContext(StatusContext)
|
const { status, spoilerHidden, disableDetails, inThread } = useContext(StatusContext)
|
||||||
if (!status || !status.card) return null
|
if (!status || !status.card) return null
|
||||||
|
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
if (status.card.url.includes('://neodb.social/') && i18n.language === 'zh-hans') {
|
||||||
|
return <CardNeodb card={status.card} />
|
||||||
|
}
|
||||||
|
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
@ -2,9 +2,9 @@ import { displayMessage } from '@components/Message'
|
|||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||||
import { getAccountStorage } from '@utils/storage/actions'
|
import { getAccountStorage } from '@utils/storage/actions'
|
||||||
|
import * as Linking from 'expo-linking'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert } from 'react-native'
|
import { Alert } from 'react-native'
|
||||||
import parse from 'url-parse'
|
|
||||||
|
|
||||||
const menuInstance = ({
|
const menuInstance = ({
|
||||||
status,
|
status,
|
||||||
@ -32,9 +32,9 @@ const menuInstance = ({
|
|||||||
|
|
||||||
const menus: ContextMenu = []
|
const menus: ContextMenu = []
|
||||||
|
|
||||||
const instance = parse(status.uri).hostname
|
const instance = Linking.parse(status.uri).hostname
|
||||||
|
|
||||||
if (instance !== getAccountStorage.string('auth.domain')) {
|
if (instance && instance !== getAccountStorage.string('auth.domain')) {
|
||||||
menus.push([
|
menus.push([
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
|
@ -60,8 +60,8 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
|
|||||||
<Button
|
<Button
|
||||||
type='text'
|
type='text'
|
||||||
content={t('componentContextMenu:hashtag.follow.action', {
|
content={t('componentContextMenu:hashtag.follow.action', {
|
||||||
defaultValue: 'fase',
|
defaultValue: 'true',
|
||||||
context: 'false'
|
context: 'true'
|
||||||
})}
|
})}
|
||||||
onPress={() => mutation.mutate({ tag_name: item.name, to: !item.following })}
|
onPress={() => mutation.mutate({ tag_name: item.name, to: !item.following })}
|
||||||
/>
|
/>
|
||||||
|
@ -7,6 +7,7 @@ import ScreenAnnouncements from '@screens/Announcements'
|
|||||||
import ScreenCompose from '@screens/Compose'
|
import ScreenCompose from '@screens/Compose'
|
||||||
import ScreenImagesViewer from '@screens/ImageViewer'
|
import ScreenImagesViewer from '@screens/ImageViewer'
|
||||||
import ScreenTabs from '@screens/Tabs'
|
import ScreenTabs from '@screens/Tabs'
|
||||||
|
import { useLinking } from '@utils/linking'
|
||||||
import navigationRef from '@utils/navigation/navigationRef'
|
import navigationRef from '@utils/navigation/navigationRef'
|
||||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||||
import pushUseConnect from '@utils/push/useConnect'
|
import pushUseConnect from '@utils/push/useConnect'
|
||||||
@ -78,32 +79,8 @@ const Screens: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deep linking for compose
|
// Deep linking
|
||||||
const [deeplinked, setDeeplinked] = useState(false)
|
useLinking()
|
||||||
useEffect(() => {
|
|
||||||
const getUrlAsync = async () => {
|
|
||||||
setDeeplinked(true)
|
|
||||||
|
|
||||||
const initialUrl = await Linking.parseInitialURLAsync()
|
|
||||||
|
|
||||||
if (initialUrl.path) {
|
|
||||||
const paths = initialUrl.path.split('/')
|
|
||||||
|
|
||||||
if (paths.length) {
|
|
||||||
if (accountActive && !accounts?.includes(accountActive)) {
|
|
||||||
setAccount(accountActive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialUrl.hostname === 'compose') {
|
|
||||||
navigationRef.navigate('Screen-Compose')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!deeplinked) {
|
|
||||||
getUrlAsync()
|
|
||||||
}
|
|
||||||
}, [accounts, accountActive, deeplinked])
|
|
||||||
|
|
||||||
// Share Extension
|
// Share Extension
|
||||||
const handleShare = (
|
const handleShare = (
|
||||||
|
@ -2,7 +2,7 @@ import { mapEnvironment } from '@utils/helpers/checkEnvironment'
|
|||||||
import { GLOBAL } from '@utils/storage'
|
import { GLOBAL } from '@utils/storage'
|
||||||
import { setGlobalStorage } from '@utils/storage/actions'
|
import { setGlobalStorage } from '@utils/storage/actions'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import parse from 'url-parse'
|
import * as Linking from 'expo-linking'
|
||||||
import { userAgent } from '.'
|
import { userAgent } from '.'
|
||||||
|
|
||||||
const list = [
|
const list = [
|
||||||
@ -86,8 +86,9 @@ export const connectMedia = (args?: {
|
|||||||
}): { uri?: string; headers?: { 'x-tooot-domain': string } } => {
|
}): { uri?: string; headers?: { 'x-tooot-domain': string } } => {
|
||||||
if (GLOBAL.connect) {
|
if (GLOBAL.connect) {
|
||||||
if (args?.uri) {
|
if (args?.uri) {
|
||||||
const host = parse(args.uri).host
|
const host = Linking.parse(args.uri).hostname
|
||||||
return {
|
return host
|
||||||
|
? {
|
||||||
...args,
|
...args,
|
||||||
uri: args.uri.replace(
|
uri: args.uri.replace(
|
||||||
host,
|
host,
|
||||||
@ -101,6 +102,7 @@ export const connectMedia = (args?: {
|
|||||||
),
|
),
|
||||||
headers: { 'x-tooot-domain': host }
|
headers: { 'x-tooot-domain': host }
|
||||||
}
|
}
|
||||||
|
: { ...args }
|
||||||
} else {
|
} else {
|
||||||
return { ...args }
|
return { ...args }
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ import { GLOBAL } from '@utils/storage'
|
|||||||
import { setGlobalStorage } from '@utils/storage/actions'
|
import { setGlobalStorage } from '@utils/storage/actions'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import Constants from 'expo-constants'
|
import Constants from 'expo-constants'
|
||||||
|
import * as Linking from 'expo-linking'
|
||||||
import { Platform } from 'react-native'
|
import { Platform } from 'react-native'
|
||||||
import parse from 'url-parse'
|
|
||||||
|
|
||||||
const userAgent = {
|
const userAgent = {
|
||||||
'User-Agent': `tooot/${Constants.expoConfig?.version} ${Platform.OS}/${Platform.Version}`
|
'User-Agent': `tooot/${Constants.expoConfig?.version} ${Platform.OS}/${Platform.Version}`
|
||||||
@ -80,17 +80,22 @@ export const parseHeaderLinks = (headerLink?: string): PagedResponse['links'] =>
|
|||||||
|
|
||||||
const linkParsed = [...headerLink.matchAll(/<(\S+?)>; *rel="(next|prev)"/gi)]
|
const linkParsed = [...headerLink.matchAll(/<(\S+?)>; *rel="(next|prev)"/gi)]
|
||||||
for (const link of linkParsed) {
|
for (const link of linkParsed) {
|
||||||
const queries = parse(link[1], true).query
|
const queries = Linking.parse(link[1]).queryParams
|
||||||
|
if (!queries) return
|
||||||
|
|
||||||
const isOffset = !!queries.offset?.length
|
const isOffset = !!queries.offset?.length
|
||||||
|
const unwrapArray = (value: any | any[]) => (Array.isArray(value) ? value[0] : value)
|
||||||
|
|
||||||
switch (link[2]) {
|
switch (link[2]) {
|
||||||
case 'prev':
|
case 'prev':
|
||||||
const prevId = isOffset ? queries.offset : queries.min_id
|
const prevId = isOffset ? queries.offset : queries.min_id
|
||||||
if (prevId) links.prev = isOffset ? { offset: prevId } : { min_id: prevId }
|
if (prevId)
|
||||||
|
links.prev = isOffset ? { offset: unwrapArray(prevId) } : { min_id: unwrapArray(prevId) }
|
||||||
break
|
break
|
||||||
case 'next':
|
case 'next':
|
||||||
const nextId = isOffset ? queries.offset : queries.max_id
|
const nextId = isOffset ? queries.offset : queries.max_id
|
||||||
if (nextId) links.next = isOffset ? { offset: nextId } : { max_id: nextId }
|
if (nextId)
|
||||||
|
links.next = isOffset ? { offset: unwrapArray(nextId) } : { max_id: unwrapArray(nextId) }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { getAccountStorage } from '@utils/storage/actions'
|
import { getAccountStorage } from '@utils/storage/actions'
|
||||||
import parse from 'url-parse'
|
import * as Linking from 'expo-linking'
|
||||||
|
|
||||||
// Would mess with the /@username format
|
// Would mess with the /@username format
|
||||||
const BLACK_LIST = ['matters.news', 'medium.com']
|
const BLACK_LIST = ['matters.news', 'medium.com']
|
||||||
@ -13,8 +13,8 @@ export const urlMatcher = (
|
|||||||
status?: Partial<Pick<Mastodon.Status, 'id' | '_remote'>>
|
status?: Partial<Pick<Mastodon.Status, 'id' | '_remote'>>
|
||||||
}
|
}
|
||||||
| undefined => {
|
| undefined => {
|
||||||
const parsed = parse(url)
|
const parsed = Linking.parse(url)
|
||||||
if (!parsed.hostname.length || !parsed.pathname.length) return undefined
|
if (!parsed.hostname?.length || !parsed.path?.length) return undefined
|
||||||
|
|
||||||
const domain = parsed.hostname
|
const domain = parsed.hostname
|
||||||
if (BLACK_LIST.includes(domain)) {
|
if (BLACK_LIST.includes(domain)) {
|
||||||
@ -26,8 +26,8 @@ export const urlMatcher = (
|
|||||||
let statusId: string | undefined
|
let statusId: string | undefined
|
||||||
let accountAcct: string | undefined
|
let accountAcct: string | undefined
|
||||||
|
|
||||||
const segments = parsed.pathname.split('/')
|
const segments = parsed.path.split('/')
|
||||||
const last = segments[segments.length - 1]
|
const last = segments.at(-1)
|
||||||
const length = segments.length // there is a starting slash
|
const length = segments.length // there is a starting slash
|
||||||
|
|
||||||
const testAndAssignStatusId = (id: string) => {
|
const testAndAssignStatusId = (id: string) => {
|
||||||
@ -38,7 +38,7 @@ export const urlMatcher = (
|
|||||||
|
|
||||||
switch (last?.startsWith('@')) {
|
switch (last?.startsWith('@')) {
|
||||||
case true:
|
case true:
|
||||||
if (length === 2 || (length === 3 && segments[length - 2] === 'web')) {
|
if (length === 1 || (length === 2 && segments.at(-2) === 'web')) {
|
||||||
// https://social.xmflsct.com/@tooot <- Mastodon v4.0 and above
|
// https://social.xmflsct.com/@tooot <- Mastodon v4.0 and above
|
||||||
// https://social.xmflsct.com/web/@tooot <- Mastodon v3.5 and below ! cannot be searched on the same instance
|
// https://social.xmflsct.com/web/@tooot <- Mastodon v3.5 and below ! cannot be searched on the same instance
|
||||||
accountAcct = `${last}@${domain}`
|
accountAcct = `${last}@${domain}`
|
||||||
@ -48,13 +48,13 @@ export const urlMatcher = (
|
|||||||
const nextToLast = segments[length - 2]
|
const nextToLast = segments[length - 2]
|
||||||
if (nextToLast) {
|
if (nextToLast) {
|
||||||
if (nextToLast === 'statuses') {
|
if (nextToLast === 'statuses') {
|
||||||
if (length === 4 && segments[length - 3] === 'web') {
|
if (length === 3 && segments.at(-3) === 'web') {
|
||||||
// https://social.xmflsct.com/web/statuses/105590085754428765 <- old
|
// https://social.xmflsct.com/web/statuses/105590085754428765 <- old
|
||||||
testAndAssignStatusId(last)
|
testAndAssignStatusId(last)
|
||||||
} else if (
|
} else if (
|
||||||
length === 5 &&
|
length === 4 &&
|
||||||
segments[length - 2] === 'statuses' &&
|
segments.at(-2) === 'statuses' &&
|
||||||
segments[length - 4] === 'users'
|
segments.at(-4) === 'users'
|
||||||
) {
|
) {
|
||||||
// https://social.xmflsct.com/users/tooot/statuses/105590085754428765 <- default Mastodon
|
// https://social.xmflsct.com/users/tooot/statuses/105590085754428765 <- default Mastodon
|
||||||
testAndAssignStatusId(last)
|
testAndAssignStatusId(last)
|
||||||
@ -62,7 +62,7 @@ export const urlMatcher = (
|
|||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
nextToLast.startsWith('@') &&
|
nextToLast.startsWith('@') &&
|
||||||
(length === 3 || (length === 4 && segments[length - 3] === 'web'))
|
(length === 2 || (length === 3 && segments.at(-3) === 'web'))
|
||||||
) {
|
) {
|
||||||
// https://social.xmflsct.com/web/@tooot/105590085754428765 <- pretty Mastodon v3.5 and below
|
// https://social.xmflsct.com/web/@tooot/105590085754428765 <- pretty Mastodon v3.5 and below
|
||||||
// https://social.xmflsct.com/@tooot/105590085754428765 <- pretty Mastodon v4.0 and above
|
// https://social.xmflsct.com/@tooot/105590085754428765 <- pretty Mastodon v4.0 and above
|
||||||
|
43
src/utils/linking/index.ts
Normal file
43
src/utils/linking/index.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import openLink from '@components/openLink'
|
||||||
|
import navigationRef from '@utils/navigation/navigationRef'
|
||||||
|
import { getReadableAccounts, setAccount } from '@utils/storage/actions'
|
||||||
|
import * as Linking from 'expo-linking'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
// /compose OR /compose/@username@example.com
|
||||||
|
|
||||||
|
export const useLinking = () => {
|
||||||
|
const parseLink = async (link: string | null) => {
|
||||||
|
if (!link) return
|
||||||
|
|
||||||
|
const parsed = Linking.parse(link)
|
||||||
|
|
||||||
|
switch (parsed.scheme) {
|
||||||
|
case 'tooot':
|
||||||
|
if (parsed.hostname === 'compose') {
|
||||||
|
if (parsed.path?.length) {
|
||||||
|
const accounts = getReadableAccounts()
|
||||||
|
const foundNotActiveAccount = accounts.find(
|
||||||
|
account => account.acct === parsed.path && !account.active
|
||||||
|
)
|
||||||
|
if (foundNotActiveAccount) {
|
||||||
|
await setAccount(foundNotActiveAccount.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navigationRef.navigate('Screen-Compose')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'https':
|
||||||
|
case 'http':
|
||||||
|
await openLink(link)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Linking.getInitialURL().then(parseLink)
|
||||||
|
|
||||||
|
const listener = Linking.addEventListener('url', ({ url }) => parseLink(url))
|
||||||
|
return () => listener.remove()
|
||||||
|
}, [])
|
||||||
|
}
|
@ -9,13 +9,22 @@ export type QueryKeyInstance = ['Instance'] | ['Instance', { domain?: string }]
|
|||||||
|
|
||||||
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyInstance>) => {
|
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyInstance>) => {
|
||||||
const domain = queryKey[1]?.domain
|
const domain = queryKey[1]?.domain
|
||||||
|
|
||||||
|
const checkV2Format = (body: Mastodon.Instance_V2) => {
|
||||||
|
if (body.version) {
|
||||||
|
return body
|
||||||
|
} else {
|
||||||
|
throw new Error('Instance v2 format error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (domain) {
|
if (domain) {
|
||||||
return await apiGeneral<Mastodon.Instance<'v2'>>({
|
return await apiGeneral<Mastodon.Instance<'v2'>>({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
domain,
|
domain,
|
||||||
url: 'api/v2/instance'
|
url: 'api/v2/instance'
|
||||||
})
|
})
|
||||||
.then(res => res.body)
|
.then(res => checkV2Format(res.body))
|
||||||
.catch(
|
.catch(
|
||||||
async () =>
|
async () =>
|
||||||
await apiGeneral<Mastodon.Instance<'v1'>>({
|
await apiGeneral<Mastodon.Instance<'v1'>>({
|
||||||
@ -32,7 +41,7 @@ const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyInstance
|
|||||||
version: 'v2',
|
version: 'v2',
|
||||||
url: 'instance'
|
url: 'instance'
|
||||||
})
|
})
|
||||||
.then(res => res.body)
|
.then(res => checkV2Format(res.body))
|
||||||
.catch(
|
.catch(
|
||||||
async () =>
|
async () =>
|
||||||
await apiInstance<Mastodon.Instance<'v1'>>({
|
await apiInstance<Mastodon.Instance<'v1'>>({
|
||||||
|
40
src/utils/queryHooks/neodb.ts
Normal file
40
src/utils/queryHooks/neodb.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||||
|
import apiGeneral from '@utils/api/general'
|
||||||
|
import { AxiosError } from 'axios'
|
||||||
|
|
||||||
|
export type QueryKeyNeodb = ['Neodb', { path: string }]
|
||||||
|
|
||||||
|
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyNeodb>) => {
|
||||||
|
const data: any = {}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
apiGeneral({
|
||||||
|
method: 'get',
|
||||||
|
domain: 'neodb.social',
|
||||||
|
url: `/${queryKey[1].path}`
|
||||||
|
}).then(res => {
|
||||||
|
const matches = (res.body as string).match(/"(\/media\/.+autocrop.+?)"/)
|
||||||
|
data.image = matches?.[1]
|
||||||
|
}),
|
||||||
|
apiGeneral({
|
||||||
|
method: 'get',
|
||||||
|
domain: 'neodb.social',
|
||||||
|
url: `/api/${queryKey[1].path}`
|
||||||
|
}).then(res => (data.data = res.body))
|
||||||
|
])
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNeodbQuery = (
|
||||||
|
params: QueryKeyNeodb[1] & {
|
||||||
|
options?: UseQueryOptions<any, AxiosError>
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const queryKey: QueryKeyNeodb = ['Neodb', { path: params.path }]
|
||||||
|
return useQuery(queryKey, queryFunction, {
|
||||||
|
...params?.options,
|
||||||
|
staleTime: Infinity,
|
||||||
|
cacheTime: Infinity
|
||||||
|
})
|
||||||
|
}
|
11
yarn.lock
11
yarn.lock
@ -3781,13 +3781,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/url-parse@npm:^1.4.8":
|
|
||||||
version: 1.4.8
|
|
||||||
resolution: "@types/url-parse@npm:1.4.8"
|
|
||||||
checksum: 44a5e96ed4b579c43750f3578bfa9165f97a359c3b2a85ee126e9c16db964f6ea105e152afd3d1adbd15850a8b812043215f3820112177bb4255a60b432dbd85
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/yargs-parser@npm:*":
|
"@types/yargs-parser@npm:*":
|
||||||
version: 21.0.0
|
version: 21.0.0
|
||||||
resolution: "@types/yargs-parser@npm:21.0.0"
|
resolution: "@types/yargs-parser@npm:21.0.0"
|
||||||
@ -11470,7 +11463,6 @@ __metadata:
|
|||||||
"@types/react": ^18.0.28
|
"@types/react": ^18.0.28
|
||||||
"@types/react-dom": ^18.0.11
|
"@types/react-dom": ^18.0.11
|
||||||
"@types/react-native-share-menu": ^5.0.2
|
"@types/react-native-share-menu": ^5.0.2
|
||||||
"@types/url-parse": ^1.4.8
|
|
||||||
axios: ^1.3.4
|
axios: ^1.3.4
|
||||||
babel-plugin-module-resolver: ^5.0.0
|
babel-plugin-module-resolver: ^5.0.0
|
||||||
babel-plugin-transform-remove-console: ^6.9.4
|
babel-plugin-transform-remove-console: ^6.9.4
|
||||||
@ -11524,7 +11516,6 @@ __metadata:
|
|||||||
react-native-tab-view: ^3.5.1
|
react-native-tab-view: ^3.5.1
|
||||||
rn-placeholder: ^3.0.3
|
rn-placeholder: ^3.0.3
|
||||||
typescript: ^4.9.5
|
typescript: ^4.9.5
|
||||||
url-parse: ^1.5.10
|
|
||||||
zeego: ^1.3.1
|
zeego: ^1.3.1
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
@ -11825,7 +11816,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"url-parse@npm:^1.5.10, url-parse@npm:^1.5.9":
|
"url-parse@npm:^1.5.9":
|
||||||
version: 1.5.10
|
version: 1.5.10
|
||||||
resolution: "url-parse@npm:1.5.10"
|
resolution: "url-parse@npm:1.5.10"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Reference in New Issue
Block a user