From f2de56f60280d66a12edd63330a82461582ab30c Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 12 Nov 2022 17:52:50 +0100 Subject: [PATCH] Fixed #446 --- src/components/Account.tsx | 2 + src/components/Parse/HTML.tsx | 6 +- src/components/Timeline/Default.tsx | 186 ++++++++++---------- src/components/Timeline/Shared/Card.tsx | 221 ++++++++++++++++++------ src/components/openLink.ts | 52 ++---- src/helpers/urlMatcher.ts | 50 ++++++ src/utils/queryHooks/status.ts | 26 +++ 7 files changed, 367 insertions(+), 176 deletions(-) create mode 100644 src/helpers/urlMatcher.ts create mode 100644 src/utils/queryHooks/status.ts diff --git a/src/components/Account.tsx b/src/components/Account.tsx index b3415253..103ded66 100644 --- a/src/components/Account.tsx +++ b/src/components/Account.tsx @@ -34,9 +34,11 @@ const ComponentAccount: React.FC = ({ renderNode({ @@ -247,7 +251,7 @@ const ParseHTML = React.memo( return ( - {typeof totalLines === 'number' || numberOfLines === 1 ? ( + {(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? ( { diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index d63e9bce..631c9e3b 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -16,8 +16,8 @@ import { getInstanceAccount } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import { uniqBy } from 'lodash' -import React, { useCallback, useRef } from 'react' -import { Pressable, View } from 'react-native' +import React, { useRef } from 'react' +import { Pressable, StyleProp, View, ViewStyle } from 'react-native' import { useSelector } from 'react-redux' import TimelineContextMenu from './Shared/ContextMenu' import TimelineFeedback from './Shared/Feedback' @@ -45,6 +45,10 @@ const TimelineDefault: React.FC = ({ disableDetails = false, disableOnPress = false }) => { + if (highlighted) { + disableOnPress = true + } + const { colors } = useTheme() const instanceAccount = useSelector(getInstanceAccount, () => true) const navigation = useNavigation>() @@ -63,19 +67,100 @@ const TimelineDefault: React.FC = ({ return } - const onPress = useCallback(() => { + const onPress = () => { analytics('timeline_default_press', { page: queryKey ? queryKey[1].page : origin }) - !disableOnPress && - !highlighted && - navigation.push('Tab-Shared-Toot', { - toot: actualStatus, - rootQueryKey: queryKey - }) - }, []) + navigation.push('Tab-Shared-Toot', { + toot: actualStatus, + rootQueryKey: queryKey + }) + } - return ( + const mainStyle: StyleProp = { + padding: StyleConstants.Spacing.Global.PagePadding, + backgroundColor: colors.backgroundDefault, + paddingBottom: disableDetails ? StyleConstants.Spacing.Global.PagePadding : 0 + } + const main = () => ( + <> + {item.reblog ? ( + + ) : item._pinned ? ( + + ) : null} + + + + + + + + {typeof actualStatus.content === 'string' && actualStatus.content.length > 0 ? ( + + ) : null} + {queryKey && actualStatus.poll ? ( + + ) : null} + {!disableDetails && + Array.isArray(actualStatus.media_attachments) && + actualStatus.media_attachments.length ? ( + + ) : null} + {!disableDetails && actualStatus.card ? : null} + {!disableDetails ? ( + + ) : null} + + + + + {queryKey && !disableDetails ? ( + d?.id !== instanceAccount?.id), + d => d?.id + ).map(d => d?.acct)} + reblog={item.reblog ? true : false} + /> + ) : null} + + ) + + return disableOnPress ? ( + {main()} + ) : ( = ({ > {}} > - {item.reblog ? ( - - ) : item._pinned ? ( - - ) : null} - - - - - - - - {typeof actualStatus.content === 'string' && actualStatus.content.length > 0 ? ( - - ) : null} - {queryKey && actualStatus.poll ? ( - - ) : null} - {!disableDetails && - Array.isArray(actualStatus.media_attachments) && - actualStatus.media_attachments.length ? ( - - ) : null} - {!disableDetails && actualStatus.card ? : null} - {!disableDetails ? ( - - ) : null} - - - - - {queryKey && !disableDetails ? ( - d?.id !== instanceAccount?.id), - d => d?.id - ).map(d => d?.acct)} - reblog={item.reblog ? true : false} - /> - ) : null} + {main()} ) diff --git a/src/components/Timeline/Shared/Card.tsx b/src/components/Timeline/Shared/Card.tsx index 519e8066..bbe01708 100644 --- a/src/components/Timeline/Shared/Card.tsx +++ b/src/components/Timeline/Shared/Card.tsx @@ -1,24 +1,184 @@ +import ComponentAccount from '@components/Account' import analytics from '@components/analytics' import GracefullyImage from '@components/GracefullyImage' import openLink from '@components/openLink' import CustomText from '@components/Text' +import { matchAccount, matchStatus } from '@helpers/urlMatcher' import { useNavigation } from '@react-navigation/native' +import { useAccountQuery } from '@utils/queryHooks/account' +import { useSearchQuery } from '@utils/queryHooks/search' +import { useStatusQuery } from '@utils/queryHooks/status' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React from 'react' +import React, { useEffect, useState } from 'react' import { Pressable, StyleSheet, View } from 'react-native' +import { Circle } from 'react-native-animated-spinkit' +import TimelineDefault from '../Default' export interface Props { - card: Pick< - Mastodon.Card, - 'url' | 'image' | 'blurhash' | 'title' | 'description' - > + card: Pick } const TimelineCard = React.memo(({ card }: Props) => { const { colors } = useTheme() const navigation = useNavigation() + const [loading, setLoading] = useState(false) + const isStatus = matchStatus(card.url) + const [foundStatus, setFoundStatus] = useState() + const isAccount = matchAccount(card.url) + const [foundAccount, setFoundAccount] = useState() + + const searchQuery = useSearchQuery({ + type: (() => { + if (isStatus) return 'statuses' + if (isAccount) return 'accounts' + })(), + term: (() => { + if (isStatus) { + if (isStatus.sameInstance) { + return + } else { + return card.url + } + } + if (isAccount) { + if (isAccount.sameInstance) { + if (isAccount.style === 'default') { + return + } else { + return isAccount.username + } + } else { + return card.url + } + } + })(), + limit: 1, + options: { enabled: false } + }) + + const statusQuery = useStatusQuery({ + id: isStatus?.id || '', + options: { enabled: false } + }) + useEffect(() => { + if (isStatus) { + setLoading(true) + if (isStatus.sameInstance) { + statusQuery + .refetch() + .then(res => { + res.data && setFoundStatus(res.data) + setLoading(false) + }) + .catch(() => setLoading(false)) + } else { + searchQuery + .refetch() + .then(res => { + const status = (res.data as any)?.statuses?.[0] + status && setFoundStatus(status) + setLoading(false) + }) + .catch(() => setLoading(false)) + } + } + }, []) + + const accountQuery = useAccountQuery({ + id: isAccount?.style === 'default' ? isAccount.id : '', + options: { enabled: false } + }) + useEffect(() => { + if (isAccount) { + setLoading(true) + if (isAccount.sameInstance && isAccount.style === 'default') { + accountQuery + .refetch() + .then(res => { + res.data && setFoundAccount(res.data) + setLoading(false) + }) + .catch(() => setLoading(false)) + } else { + searchQuery + .refetch() + .then(res => { + const account = (res.data as any)?.accounts?.[0] + account && setFoundAccount(account) + setLoading(false) + }) + .catch(() => setLoading(false)) + } + } + }, []) + + const cardContent = () => { + if (loading) { + return ( + + + + ) + } + if (isStatus && foundStatus) { + return + } + if (isAccount && foundAccount) { + return + } + return ( + <> + {card.image ? ( + + ) : null} + + + {card.title} + + {card.description ? ( + + {card.description} + + ) : null} + + {card.url} + + + + ) + } + return ( { style={{ flex: 1, flexDirection: 'row', - height: StyleConstants.Font.LineHeight.M * 5, + minHeight: isAccount && foundAccount ? undefined : StyleConstants.Font.LineHeight.M * 5, marginTop: StyleConstants.Spacing.M, borderWidth: StyleSheet.hairlineWidth, - borderRadius: 6, + borderRadius: StyleConstants.Spacing.S, overflow: 'hidden', borderColor: colors.border }} @@ -37,51 +197,8 @@ const TimelineCard = React.memo(({ card }: Props) => { analytics('timeline_shared_card_press') await openLink(card.url, navigation) }} - testID='base' - > - {card.image ? ( - - ) : null} - - - {card.title} - - {card.description ? ( - - {card.description} - - ) : null} - - {card.url} - - - + children={cardContent} + /> ) }) diff --git a/src/components/openLink.ts b/src/components/openLink.ts index f30b0372..96531e87 100644 --- a/src/components/openLink.ts +++ b/src/components/openLink.ts @@ -1,24 +1,12 @@ import apiInstance from '@api/instance' import navigationRef from '@helpers/navigationRef' +import { matchAccount, matchStatus } from '@helpers/urlMatcher' 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' -// 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?: any) => { @@ -26,10 +14,7 @@ const openLink = async (url: string, navigation?: any) => { return } - const handleNavigation = ( - page: 'Tab-Shared-Toot' | 'Tab-Shared-Account', - options: {} - ) => { + const handleNavigation = (page: 'Tab-Shared-Toot' | 'Tab-Shared-Account', options: {}) => { if (navigation) { // @ts-ignore navigation.push(page, options) @@ -40,14 +25,10 @@ const openLink = async (url: string, navigation?: any) => { } // 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] } - }) + const isStatus = matchStatus(url) + if (isStatus) { + if (isStatus.sameInstance) { + handleNavigation('Tab-Shared-Toot', { toot: { id: isStatus.id } }) return } @@ -71,15 +52,11 @@ const openLink = async (url: string, navigation?: any) => { } // If an account can be found - const matchedAccount = url.match(matcherAccount) - 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] } - }) + const isAccount = matchAccount(url) + if (isAccount) { + if (isAccount.sameInstance) { + if (isAccount.style === 'default' && isAccount.id) { + handleNavigation('Tab-Shared-Account', { account: isAccount }) return } } @@ -91,7 +68,12 @@ const openLink = async (url: string, navigation?: any) => { version: 'v2', method: 'get', url: 'search', - params: { type: 'accounts', q: url, limit: 1, resolve: true } + params: { + type: 'accounts', + q: isAccount.sameInstance && isAccount.style === 'pretty' ? isAccount.username : url, + limit: 1, + resolve: true + } }) } catch {} if (response && response.body && response.body.accounts.length) { diff --git a/src/helpers/urlMatcher.ts b/src/helpers/urlMatcher.ts new file mode 100644 index 00000000..a5d3c9d5 --- /dev/null +++ b/src/helpers/urlMatcher.ts @@ -0,0 +1,50 @@ +import { store } from '@root/store' +import { getInstanceUrl } from '@utils/slices/instancesSlice' + +const matchStatus = ( + url: string +): { id: string; style: 'default' | 'pretty'; sameInstance: boolean } | null => { + // https://social.xmflsct.com/web/statuses/105590085754428765 <- default + // https://social.xmflsct.com/@tooot/105590085754428765 <- pretty + const matcherStatus = new RegExp(/(https?:\/\/)?([^\/]+)\/(web\/statuses|@.+)\/([0-9]+)/) + + const matched = url.match(matcherStatus) + if (matched) { + const hostname = matched[2] + const style = matched[3] === 'web/statuses' ? 'default' : 'pretty' + const id = matched[4] + + const instanceUrl = getInstanceUrl(store.getState()) + return { id, style, sameInstance: hostname === instanceUrl } + } + + return null +} + +const matchAccount = ( + url: string +): + | { id: string; style: 'default'; sameInstance: boolean } + | { username: string; style: 'pretty'; sameInstance: boolean } + | null => { + // https://social.xmflsct.com/web/accounts/14195 <- default + // https://social.xmflsct.com/web/@tooot <- pretty ! cannot be searched on the same instance + // https://social.xmflsct.com/@tooot <- pretty + const matcherAccount = new RegExp(/(https?:\/\/)?([^\/]+)(\/web|\/web\/accounts)?\/([0-9]+|@.+)/) + + const matched = url.match(matcherAccount) + if (matched) { + const hostname = matched[2] + const style = matched[4].startsWith('@') ? 'pretty' : 'default' + const account = matched[4] + + const instanceUrl = getInstanceUrl(store.getState()) + return style === 'default' + ? { id: account, style, sameInstance: hostname === instanceUrl } + : { username: account, style, sameInstance: hostname === instanceUrl } + } + + return null +} + +export { matchStatus, matchAccount } diff --git a/src/utils/queryHooks/status.ts b/src/utils/queryHooks/status.ts new file mode 100644 index 00000000..c0a2de02 --- /dev/null +++ b/src/utils/queryHooks/status.ts @@ -0,0 +1,26 @@ +import apiInstance from '@api/instance' +import { AxiosError } from 'axios' +import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query' + +export type QueryKeyStatus = ['Status', { id: Mastodon.Status['id'] }] + +const queryFunction = ({ queryKey }: QueryFunctionContext) => { + const { id } = queryKey[1] + + return apiInstance({ + method: 'get', + url: `statuses/${id}` + }).then(res => res.body) +} + +const useStatusQuery = ({ + options, + ...queryKeyParams +}: QueryKeyStatus[1] & { + options?: UseQueryOptions +}) => { + const queryKey: QueryKeyStatus = ['Status', { ...queryKeyParams }] + return useQuery(queryKey, queryFunction, options) +} + +export { useStatusQuery }