mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	
							
								
								
									
										1
									
								
								src/@types/react-navigation.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/@types/react-navigation.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -60,7 +60,6 @@ declare namespace Nav {
 | 
			
		||||
        url: Mastodon.AttachmentImage['url']
 | 
			
		||||
        width?: number
 | 
			
		||||
        height?: number
 | 
			
		||||
        preview_url: Mastodon.AttachmentImage['preview_url']
 | 
			
		||||
        remote_url?: Mastodon.AttachmentImage['remote_url']
 | 
			
		||||
      }[]
 | 
			
		||||
      id: Mastodon.Attachment['id']
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { RootState } from '@root/store'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import axios, { AxiosRequestConfig } from 'axios'
 | 
			
		||||
import chalk from 'chalk'
 | 
			
		||||
import li from 'li'
 | 
			
		||||
 | 
			
		||||
@@ -14,7 +14,10 @@ export type Params = {
 | 
			
		||||
  }
 | 
			
		||||
  headers?: { [key: string]: string }
 | 
			
		||||
  body?: FormData
 | 
			
		||||
  onUploadProgress?: (progressEvent: any) => void
 | 
			
		||||
  extras?: Omit<
 | 
			
		||||
    AxiosRequestConfig,
 | 
			
		||||
    'method' | 'url' | 'params' | 'headers' | 'data'
 | 
			
		||||
  >
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const apiInstance = async <T = unknown>({
 | 
			
		||||
@@ -24,7 +27,7 @@ const apiInstance = async <T = unknown>({
 | 
			
		||||
  params,
 | 
			
		||||
  headers,
 | 
			
		||||
  body,
 | 
			
		||||
  onUploadProgress
 | 
			
		||||
  extras
 | 
			
		||||
}: Params): Promise<{ body: T; links: { prev?: string; next?: string } }> => {
 | 
			
		||||
  const { store } = require('@root/store')
 | 
			
		||||
  const state = store.getState() as RootState
 | 
			
		||||
@@ -70,7 +73,7 @@ const apiInstance = async <T = unknown>({
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    ...(body && { data: body }),
 | 
			
		||||
    ...(onUploadProgress && { onUploadProgress: onUploadProgress })
 | 
			
		||||
    ...extras
 | 
			
		||||
  })
 | 
			
		||||
    .then(response => {
 | 
			
		||||
      let prev
 | 
			
		||||
 
 | 
			
		||||
@@ -102,7 +102,11 @@ const GracefullyImage = React.memo(
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Pressable
 | 
			
		||||
        style={[style, dimension, { backgroundColor: theme.shimmerDefault }]}
 | 
			
		||||
        style={[
 | 
			
		||||
          style,
 | 
			
		||||
          dimension,
 | 
			
		||||
          { backgroundColor: theme.backgroundOverlayDefault }
 | 
			
		||||
        ]}
 | 
			
		||||
        {...(onPress
 | 
			
		||||
          ? hidden
 | 
			
		||||
            ? { disabled: true }
 | 
			
		||||
 
 | 
			
		||||
@@ -121,7 +121,7 @@ const renderNode = ({
 | 
			
		||||
            onPress={async () => {
 | 
			
		||||
              analytics('status_link_press')
 | 
			
		||||
              !disableDetails && !shouldBeTag
 | 
			
		||||
                ? await openLink(href)
 | 
			
		||||
                ? await openLink(href, navigation)
 | 
			
		||||
                : navigation.push('Tab-Shared-Hashtag', {
 | 
			
		||||
                    hashtag: content.substring(1)
 | 
			
		||||
                  })
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,6 @@ const TimelineAttachment = React.memo(
 | 
			
		||||
              imageUrls.push({
 | 
			
		||||
                id: attachment.id,
 | 
			
		||||
                url: attachment.url,
 | 
			
		||||
                preview_url: attachment.preview_url,
 | 
			
		||||
                remote_url: attachment.remote_url,
 | 
			
		||||
                width: attachment.meta?.original?.width,
 | 
			
		||||
                height: attachment.meta?.original?.height
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import analytics from '@components/analytics'
 | 
			
		||||
import GracefullyImage from '@components/GracefullyImage'
 | 
			
		||||
import openLink from '@components/openLink'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
@@ -13,13 +14,14 @@ export interface Props {
 | 
			
		||||
const TimelineCard = React.memo(
 | 
			
		||||
  ({ card }: Props) => {
 | 
			
		||||
    const { theme } = useTheme()
 | 
			
		||||
    const navigation = useNavigation()
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Pressable
 | 
			
		||||
        style={[styles.card, { borderColor: theme.border }]}
 | 
			
		||||
        onPress={async () => {
 | 
			
		||||
          analytics('timeline_shared_card_press')
 | 
			
		||||
          await openLink(card.url)
 | 
			
		||||
          await openLink(card.url, navigation)
 | 
			
		||||
        }}
 | 
			
		||||
        testID='base'
 | 
			
		||||
      >
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,127 @@
 | 
			
		||||
import apiInstance from '@api/instance'
 | 
			
		||||
import { NavigationProp, ParamListBase } from '@react-navigation/native'
 | 
			
		||||
import { navigationRef } from '@root/Screens'
 | 
			
		||||
import { store } from '@root/store'
 | 
			
		||||
import { SearchResult } from '@utils/queryHooks/search'
 | 
			
		||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
 | 
			
		||||
import { getSettingsBrowser } from '@utils/slices/settingsSlice'
 | 
			
		||||
import * as Linking from 'expo-linking'
 | 
			
		||||
import * as WebBrowser from 'expo-web-browser'
 | 
			
		||||
 | 
			
		||||
const openLink = async (url: string) => {
 | 
			
		||||
// https://social.xmflsct.com/web/statuses/105590085754428765 <- default
 | 
			
		||||
// https://social.xmflsct.com/@tooot/105590085754428765 <- pretty
 | 
			
		||||
const matcherStatus = new RegExp(
 | 
			
		||||
  /http[s]?:\/\/(.*)\/(web\/statuses|@.*)\/([0-9]*)/
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://social.xmflsct.com/web/accounts/14195 <- default
 | 
			
		||||
// https://social.xmflsct.com/@tooot <- pretty
 | 
			
		||||
const matcherAccount = new RegExp(
 | 
			
		||||
  /http[s]?:\/\/(.*)\/(web\/accounts\/([0-9]*)|@.*)/
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export let loadingLink = false
 | 
			
		||||
 | 
			
		||||
const openLink = async (
 | 
			
		||||
  url: string,
 | 
			
		||||
  navigation?: NavigationProp<
 | 
			
		||||
    ParamListBase,
 | 
			
		||||
    string,
 | 
			
		||||
    Readonly<{
 | 
			
		||||
      key: string
 | 
			
		||||
      index: number
 | 
			
		||||
      routeNames: string[]
 | 
			
		||||
      history?: unknown[] | undefined
 | 
			
		||||
      routes: any[]
 | 
			
		||||
      type: string
 | 
			
		||||
      stale: false
 | 
			
		||||
    }>,
 | 
			
		||||
    {},
 | 
			
		||||
    {}
 | 
			
		||||
  >
 | 
			
		||||
) => {
 | 
			
		||||
  if (loadingLink) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleNavigation = (
 | 
			
		||||
    page: 'Tab-Shared-Toot' | 'Tab-Shared-Account',
 | 
			
		||||
    options: {}
 | 
			
		||||
  ) => {
 | 
			
		||||
    if (navigation) {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      navigation.push(page, options)
 | 
			
		||||
    } else {
 | 
			
		||||
      navigationRef.current?.navigate(page, options)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If a tooot can be found
 | 
			
		||||
  const matchedStatus = url.match(matcherStatus)
 | 
			
		||||
  if (matchedStatus) {
 | 
			
		||||
    // If the link in current instance
 | 
			
		||||
    const instanceUrl = getInstanceUrl(store.getState())
 | 
			
		||||
    if (matchedStatus[1] === instanceUrl) {
 | 
			
		||||
      handleNavigation('Tab-Shared-Toot', {
 | 
			
		||||
        toot: { id: matchedStatus[3] }
 | 
			
		||||
      })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    loadingLink = true
 | 
			
		||||
    let response
 | 
			
		||||
    try {
 | 
			
		||||
      response = await apiInstance<SearchResult>({
 | 
			
		||||
        version: 'v2',
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: 'search',
 | 
			
		||||
        params: { type: 'statuses', q: url, limit: 1, resolve: true }
 | 
			
		||||
      })
 | 
			
		||||
    } catch {}
 | 
			
		||||
    if (response && response.body && response.body.statuses.length) {
 | 
			
		||||
      handleNavigation('Tab-Shared-Toot', {
 | 
			
		||||
        toot: response.body.statuses[0]
 | 
			
		||||
      })
 | 
			
		||||
      loadingLink = false
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If an account can be found
 | 
			
		||||
  const matchedAccount = url.match(matcherAccount)
 | 
			
		||||
  console.log(matchedAccount)
 | 
			
		||||
  if (matchedAccount) {
 | 
			
		||||
    // If the link in current instance
 | 
			
		||||
    const instanceUrl = getInstanceUrl(store.getState())
 | 
			
		||||
    if (matchedAccount[1] === instanceUrl) {
 | 
			
		||||
      if (matchedAccount[3] && matchedAccount[3].match(/[0-9]*/)) {
 | 
			
		||||
        handleNavigation('Tab-Shared-Account', {
 | 
			
		||||
          account: { id: matchedAccount[3] }
 | 
			
		||||
        })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    loadingLink = true
 | 
			
		||||
    let response
 | 
			
		||||
    try {
 | 
			
		||||
      response = await apiInstance<SearchResult>({
 | 
			
		||||
        version: 'v2',
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: 'search',
 | 
			
		||||
        params: { type: 'accounts', q: url, limit: 1, resolve: true }
 | 
			
		||||
      })
 | 
			
		||||
    } catch {}
 | 
			
		||||
    if (response && response.body && response.body.accounts.length) {
 | 
			
		||||
      handleNavigation('Tab-Shared-Account', {
 | 
			
		||||
        account: response.body.accounts[0]
 | 
			
		||||
      })
 | 
			
		||||
      loadingLink = false
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadingLink = false
 | 
			
		||||
  switch (getSettingsBrowser(store.getState())) {
 | 
			
		||||
    case 'internal':
 | 
			
		||||
      await WebBrowser.openBrowserAsync(url, {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,12 +15,3 @@ export type Position = {
 | 
			
		||||
  x: number
 | 
			
		||||
  y: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ImageSource = {
 | 
			
		||||
  id: string
 | 
			
		||||
  preview_url: string
 | 
			
		||||
  remote_url?: string
 | 
			
		||||
  url: string
 | 
			
		||||
  width?: number
 | 
			
		||||
  height?: number
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,17 +14,18 @@ import {
 | 
			
		||||
  View,
 | 
			
		||||
  VirtualizedList
 | 
			
		||||
} from 'react-native'
 | 
			
		||||
import { ImageSource } from './@types'
 | 
			
		||||
import ImageItem from './components/ImageItem'
 | 
			
		||||
import useAnimatedComponents from './hooks/useAnimatedComponents'
 | 
			
		||||
import useImageIndexChange from './hooks/useImageIndexChange'
 | 
			
		||||
import useRequestClose from './hooks/useRequestClose'
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  images: ImageSource[]
 | 
			
		||||
  images: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls']
 | 
			
		||||
  imageIndex: number
 | 
			
		||||
  onRequestClose: () => void
 | 
			
		||||
  onLongPress?: (image: ImageSource) => void
 | 
			
		||||
  onLongPress?: (
 | 
			
		||||
    image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
 | 
			
		||||
  ) => void
 | 
			
		||||
  onImageIndexChange?: (imageIndex: number) => void
 | 
			
		||||
  backgroundColor?: string
 | 
			
		||||
  swipeToCloseEnabled?: boolean
 | 
			
		||||
@@ -48,7 +49,11 @@ function ImageViewer ({
 | 
			
		||||
  delayLongPress = DEFAULT_DELAY_LONG_PRESS,
 | 
			
		||||
  HeaderComponent
 | 
			
		||||
}: Props) {
 | 
			
		||||
  const imageList = React.createRef<VirtualizedList<ImageSource>>()
 | 
			
		||||
  const imageList = React.createRef<
 | 
			
		||||
    VirtualizedList<
 | 
			
		||||
      Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
 | 
			
		||||
    >
 | 
			
		||||
  >()
 | 
			
		||||
  const [opacity, onRequestCloseEnhanced] = useRequestClose(onRequestClose)
 | 
			
		||||
  const [currentImageIndex, onScroll] = useImageIndexChange(imageIndex, SCREEN)
 | 
			
		||||
  const [headerTransform, toggleBarsVisible] = useAnimatedComponents()
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@
 | 
			
		||||
import GracefullyImage from '@components/GracefullyImage'
 | 
			
		||||
import React, { useState, useCallback } from 'react'
 | 
			
		||||
import { Animated, Dimensions, StyleSheet } from 'react-native'
 | 
			
		||||
import { ImageSource } from '../@types'
 | 
			
		||||
import usePanResponder from '../hooks/usePanResponder'
 | 
			
		||||
import { getImageStyles, getImageTransform } from '../utils'
 | 
			
		||||
 | 
			
		||||
@@ -18,10 +17,12 @@ const SCREEN_WIDTH = SCREEN.width
 | 
			
		||||
const SCREEN_HEIGHT = SCREEN.height
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  imageSrc: ImageSource
 | 
			
		||||
  imageSrc: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
 | 
			
		||||
  onRequestClose: () => void
 | 
			
		||||
  onZoom: (isZoomed: boolean) => void
 | 
			
		||||
  onLongPress: (image: ImageSource) => void
 | 
			
		||||
  onLongPress: (
 | 
			
		||||
    image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
 | 
			
		||||
  ) => void
 | 
			
		||||
  delayLongPress: number
 | 
			
		||||
  swipeToCloseEnabled?: boolean
 | 
			
		||||
  doubleTapToZoomEnabled?: boolean
 | 
			
		||||
@@ -89,7 +90,6 @@ const ImageItem = ({
 | 
			
		||||
        children={
 | 
			
		||||
          <GracefullyImage
 | 
			
		||||
            uri={{
 | 
			
		||||
              preview: imageSrc.preview_url,
 | 
			
		||||
              original: imageSrc.url,
 | 
			
		||||
              remote: imageSrc.remote_url
 | 
			
		||||
            }}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ import {
 | 
			
		||||
  State,
 | 
			
		||||
  TapGestureHandler
 | 
			
		||||
} from 'react-native-gesture-handler'
 | 
			
		||||
import { ImageSource } from '../@types'
 | 
			
		||||
import useDoubleTapToZoom from '../hooks/useDoubleTapToZoom'
 | 
			
		||||
import { getImageStyles, getImageTransform } from '../utils'
 | 
			
		||||
 | 
			
		||||
@@ -32,10 +31,12 @@ const SCREEN_WIDTH = SCREEN.width
 | 
			
		||||
const SCREEN_HEIGHT = SCREEN.height
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  imageSrc: ImageSource
 | 
			
		||||
  imageSrc: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
 | 
			
		||||
  onRequestClose: () => void
 | 
			
		||||
  onZoom: (scaled: boolean) => void
 | 
			
		||||
  onLongPress: (image: ImageSource) => void
 | 
			
		||||
  onLongPress: (
 | 
			
		||||
    image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
 | 
			
		||||
  ) => void
 | 
			
		||||
  swipeToCloseEnabled?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -144,7 +145,6 @@ const ImageItem = ({
 | 
			
		||||
              children={
 | 
			
		||||
                <GracefullyImage
 | 
			
		||||
                  uri={{
 | 
			
		||||
                    preview: imageSrc.preview_url,
 | 
			
		||||
                    original: imageSrc.url,
 | 
			
		||||
                    remote: imageSrc.remote_url
 | 
			
		||||
                  }}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { useMemo, useEffect, useCallback } from 'react'
 | 
			
		||||
import { useMemo, useEffect } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  Animated,
 | 
			
		||||
  Dimensions,
 | 
			
		||||
 
 | 
			
		||||
@@ -20,15 +20,9 @@ import {
 | 
			
		||||
} from 'react-native-safe-area-context'
 | 
			
		||||
import ImageViewer from './ImageViewer/Root'
 | 
			
		||||
 | 
			
		||||
type ImageUrl = {
 | 
			
		||||
  url: string
 | 
			
		||||
  width?: number | undefined
 | 
			
		||||
  height?: number | undefined
 | 
			
		||||
  preview_url: string
 | 
			
		||||
  remote_url?: string | undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const saveImage = async (image: ImageUrl) => {
 | 
			
		||||
const saveImage = async (
 | 
			
		||||
  image: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
 | 
			
		||||
) => {
 | 
			
		||||
  const hasAndroidPermission = async () => {
 | 
			
		||||
    const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
 | 
			
		||||
 | 
			
		||||
@@ -44,9 +38,17 @@ const saveImage = async (image: ImageUrl) => {
 | 
			
		||||
  if (Platform.OS === 'android' && !(await hasAndroidPermission())) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  CameraRoll.save(image.url || image.remote_url || image.preview_url)
 | 
			
		||||
  CameraRoll.save(image.url)
 | 
			
		||||
    .then(() => haptics('Success'))
 | 
			
		||||
    .catch(() => haptics('Error'))
 | 
			
		||||
    .catch(() => {
 | 
			
		||||
      if (image.remote_url) {
 | 
			
		||||
        CameraRoll.save(image.remote_url)
 | 
			
		||||
          .then(() => haptics('Success'))
 | 
			
		||||
          .catch(() => haptics('Error'))
 | 
			
		||||
      } else {
 | 
			
		||||
        haptics('Error')
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const HeaderComponent = React.memo(
 | 
			
		||||
@@ -57,7 +59,7 @@ const HeaderComponent = React.memo(
 | 
			
		||||
  }: {
 | 
			
		||||
    navigation: ScreenImagesViewerProp['navigation']
 | 
			
		||||
    currentIndex: number
 | 
			
		||||
    imageUrls: ImageUrl[]
 | 
			
		||||
    imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls']
 | 
			
		||||
  }) => {
 | 
			
		||||
    const insets = useSafeAreaInsets()
 | 
			
		||||
    const { t } = useTranslation('screenImageViewer')
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ export type QueryKey = [
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
type SearchResult = {
 | 
			
		||||
export type SearchResult = {
 | 
			
		||||
  accounts: Mastodon.Account[]
 | 
			
		||||
  hashtags: Mastodon.Tag[]
 | 
			
		||||
  statuses: Mastodon.Status[]
 | 
			
		||||
@@ -23,7 +23,12 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
 | 
			
		||||
    version: 'v2',
 | 
			
		||||
    method: 'get',
 | 
			
		||||
    url: 'search',
 | 
			
		||||
    params: { ...(type && { type }), ...(term && { q: term }), limit }
 | 
			
		||||
    params: {
 | 
			
		||||
      ...(type && { type }),
 | 
			
		||||
      ...(term && { q: term }),
 | 
			
		||||
      limit,
 | 
			
		||||
      resolve: true
 | 
			
		||||
    }
 | 
			
		||||
  }).then(res => res.body)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user