diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx
index 4ff3436f..246f839a 100644
--- a/src/components/Parse/HTML.tsx
+++ b/src/components/Parse/HTML.tsx
@@ -15,7 +15,7 @@ import { ElementType, parseDocument } from 'htmlparser2'
import i18next from 'i18next'
import React, { useContext, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { Platform, Pressable, Text, TextStyleIOS, View } from 'react-native'
+import { Platform, Pressable, Text, View } from 'react-native'
export interface Props {
content: string
@@ -78,8 +78,8 @@ const ParseHTML: React.FC = ({
return node.data
case ElementType.Tag:
if (node.name === 'span') {
- if (node.attribs.class?.includes('invisible')) return ''
- if (node.attribs.class?.includes('ellipsis'))
+ if (node.attribs.class?.includes('invisible') && !showFullLink) return ''
+ if (node.attribs.class?.includes('ellipsis') && !showFullLink)
return node.children.map(child => unwrapNode(child)).join('') + '...'
}
return node.children.map(child => unwrapNode(child)).join('')
@@ -87,15 +87,26 @@ const ParseHTML: React.FC = ({
return ''
}
}
- const startingOfText = useRef(false)
+ const openingMentions = useRef(true)
const renderNode = (node: ChildNode, index: number) => {
switch (node.type) {
case ElementType.Text:
- node.data.trim().length && (startingOfText.current = true) // Removing empty spaces appeared between tags and mentions
+ let content: string = node.data
+ if (openingMentions.current) {
+ if (node.data.trim().length) {
+ openingMentions.current = false // Removing empty spaces appeared between tags and mentions
+ content = excludeMentions?.current.length
+ ? node.data.replace(new RegExp(/^\s+/), '')
+ : node.data
+ } else {
+ content = node.data.trim()
+ }
+ }
+
return (
= ({
const href = node.attribs.href
if (classes) {
if (classes.includes('hashtag')) {
+ openingMentions.current = false
const tag = href.match(new RegExp(/\/tags?\/(.*)/, 'i'))?.[1].toLowerCase()
const paramsHashtag = (params as { hashtag: Mastodon.Tag['name'] } | undefined)
?.hashtag
@@ -142,7 +154,6 @@ const ParseHTML: React.FC = ({
)
if (
matchedMention &&
- !startingOfText.current &&
excludeMentions?.current.find(eM => eM.id === matchedMention.id)
) {
return null
@@ -165,6 +176,7 @@ const ParseHTML: React.FC = ({
}
}
+ openingMentions.current = false
const content = node.children.map(child => unwrapNode(child)).join('')
const shouldBeTag = status?.tags?.find(tag => `#${tag.name}` === content)
return (
@@ -182,7 +194,7 @@ const ParseHTML: React.FC = ({
}
}
}}
- children={content !== href ? content : showFullLink ? href : content}
+ children={content}
/>
)
break
diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx
index 26c8da51..ae264a35 100644
--- a/src/components/Timeline/Default.tsx
+++ b/src/components/Timeline/Default.tsx
@@ -37,6 +37,7 @@ export interface Props {
disableDetails?: boolean
disableOnPress?: boolean
isConversation?: boolean
+ isRemote?: boolean
}
// When the poll is long
@@ -47,7 +48,8 @@ const TimelineDefault: React.FC = ({
highlighted = false,
disableDetails = false,
disableOnPress = false,
- isConversation = false
+ isConversation = false,
+ isRemote = false
}) => {
const status = item.reblog ? item.reblog : item
const rawContent = useRef([])
@@ -175,7 +177,8 @@ const TimelineDefault: React.FC = ({
inThread: queryKey?.[1].page === 'Toot',
disableDetails,
disableOnPress,
- isConversation
+ isConversation,
+ isRemote
}}
>
{disableOnPress ? (
diff --git a/src/components/Timeline/Shared/Context.tsx b/src/components/Timeline/Shared/Context.tsx
index 80f1079e..ea689fc2 100644
--- a/src/components/Timeline/Shared/Context.tsx
+++ b/src/components/Timeline/Shared/Context.tsx
@@ -21,6 +21,7 @@ type StatusContextType = {
disableDetails?: boolean
disableOnPress?: boolean
isConversation?: boolean
+ isRemote?: boolean
}
const StatusContext = createContext({} as StatusContextType)
diff --git a/src/components/Timeline/Shared/HeaderDefault.tsx b/src/components/Timeline/Shared/HeaderDefault.tsx
index c96bf9ef..e5795207 100644
--- a/src/components/Timeline/Shared/HeaderDefault.tsx
+++ b/src/components/Timeline/Shared/HeaderDefault.tsx
@@ -17,7 +17,7 @@ import HeaderSharedReplies from './HeaderShared/Replies'
import HeaderSharedVisibility from './HeaderShared/Visibility'
const TimelineHeaderDefault: React.FC = () => {
- const { queryKey, rootQueryKey, status, highlighted, disableDetails, rawContent } =
+ const { queryKey, rootQueryKey, status, highlighted, disableDetails, rawContent, isRemote } =
useContext(StatusContext)
if (!status) return null
@@ -58,6 +58,14 @@ const TimelineHeaderDefault: React.FC = () => {
: { marginTop: StyleConstants.Spacing.XS, marginBottom: StyleConstants.Spacing.S })
}}
>
+ {isRemote ? (
+
+ ) : null}
{
@@ -11,7 +11,7 @@ const HeaderSharedReplies: React.FC = () => {
if (!isConversation) return null
const navigation = useNavigation()
- const { t } = useTranslation('componentTimeline')
+ const { t } = useTranslation(['common', 'componentTimeline'])
const { colors } = useTheme()
const mentionsBeginning = rawContent?.current?.[0]
@@ -26,25 +26,27 @@ const HeaderSharedReplies: React.FC = () => {
return excludeMentions?.current.length ? (
- <>
- {t('shared.header.shared.replies')}
- {excludeMentions.current.map((mention, index) => (
-
- {' '}
- navigation.push('Tab-Shared-Account', { account: mention })}
- />
-
- ))}
- >
+
+ {excludeMentions.current.map((mention, index) => (
+
+ {index > 0 ? t('common:separator') : null}
+ navigation.push('Tab-Shared-Account', { account: mention })}
+ />
+
+ ))}
+ >
+ ]}
+ />
) : null
}
diff --git a/src/i18n/en/components/contextMenu.json b/src/i18n/en/components/contextMenu.json
index f4feb500..457bba98 100644
--- a/src/i18n/en/components/contextMenu.json
+++ b/src/i18n/en/components/contextMenu.json
@@ -6,7 +6,7 @@
"action_false": "Follow user",
"action_true": "Unfollow user"
},
- "inLists": "Manage user of lists",
+ "inLists": "Lists containing user",
"showBoosts": {
"action_false": "Show user's boosts",
"action_true": "Hide users's boosts"
diff --git a/src/i18n/en/components/timeline.json b/src/i18n/en/components/timeline.json
index 362cbe3e..77492cad 100644
--- a/src/i18n/en/components/timeline.json
+++ b/src/i18n/en/components/timeline.json
@@ -122,7 +122,7 @@
"muted": {
"accessibilityLabel": "Toot muted"
},
- "replies": "Replies",
+ "replies": "Replies <0 />",
"visibility": {
"direct": {
"accessibilityLabel": "Toot is a direct message"
diff --git a/src/screens/ImageViewer/index.tsx b/src/screens/ImageViewer/index.tsx
index 140df005..28d3e9f6 100644
--- a/src/screens/ImageViewer/index.tsx
+++ b/src/screens/ImageViewer/index.tsx
@@ -4,7 +4,7 @@ import { useActionSheet } from '@expo/react-native-action-sheet'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import { RootStackScreenProps } from '@utils/navigation/navigators'
import { useTheme } from '@utils/styles/ThemeManager'
-import React, { useState } from 'react'
+import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
Dimensions,
@@ -50,6 +50,13 @@ const ScreenImagesViewer = ({
const isZoomed = useSharedValue(false)
+ const onViewableItemsChanged = useCallback(
+ ({ viewableItems }: { viewableItems: ViewToken[] }) => {
+ setCurrentIndex(viewableItems[0]?.index || 0)
+ },
+ []
+ )
+
return (
@@ -107,7 +114,7 @@ const ScreenImagesViewer = ({
/>
{
+ onActivated={() => {
showActionSheetWithOptions(
{
options: [
@@ -207,9 +214,7 @@ const ScreenImagesViewer = ({
/>
)
}}
- onViewableItemsChanged={({ viewableItems }: { viewableItems: ViewToken[] }) => {
- setCurrentIndex(viewableItems[0]?.index || 0)
- }}
+ onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={{
itemVisiblePercentThreshold: 50
}}
diff --git a/src/screens/Tabs/Public/Root.tsx b/src/screens/Tabs/Public/Root.tsx
index ac7c11e0..613e818b 100644
--- a/src/screens/Tabs/Public/Root.tsx
+++ b/src/screens/Tabs/Public/Root.tsx
@@ -83,7 +83,10 @@ const Root: React.FC null}
- onIndexChange={index => setSegment(index)}
+ onIndexChange={index => {
+ setSegment(index)
+ setGlobalStorage('app.prev_public_segment', segments[index])
+ }}
navigationState={{ index: segment, routes }}
initialLayout={{ width: Dimensions.get('window').width }}
/>
diff --git a/src/screens/Tabs/Shared/Account/Information/Fields.tsx b/src/screens/Tabs/Shared/Account/Information/Fields.tsx
index f5c083df..824739a5 100644
--- a/src/screens/Tabs/Shared/Account/Information/Fields.tsx
+++ b/src/screens/Tabs/Shared/Account/Information/Fields.tsx
@@ -1,4 +1,3 @@
-import Icon from '@components/Icon'
import { ParseHTML } from '@components/Parse'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
@@ -18,10 +17,39 @@ const AccountInformationFields: React.FC = ({ account, myInfo }) => {
const { colors } = useTheme()
return (
-
+
{account.fields.map((field, index) => (
-
-
+
+
= ({ account, myInfo }) => {
numberOfLines={5}
selectable
/>
- {field.verified_at ? (
-
- ) : null}
-
+
= ({ account, myInfo }) => {
)
}
-const styles = StyleSheet.create({
- fields: {
- borderTopWidth: StyleSheet.hairlineWidth,
- marginBottom: StyleConstants.Spacing.M
- },
- field: {
- flex: 1,
- flexDirection: 'row',
- borderBottomWidth: StyleSheet.hairlineWidth,
- paddingTop: StyleConstants.Spacing.S,
- paddingBottom: StyleConstants.Spacing.S
- },
- fieldLeft: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- borderRightWidth: 1,
- paddingLeft: StyleConstants.Spacing.S,
- paddingRight: StyleConstants.Spacing.S
- },
- fieldCheck: { marginLeft: StyleConstants.Spacing.XS },
- fieldRight: {
- flex: 2,
- justifyContent: 'center',
- paddingLeft: StyleConstants.Spacing.S,
- paddingRight: StyleConstants.Spacing.S
- }
-})
-
export default AccountInformationFields
diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx
index 8b0149cf..fef8f609 100644
--- a/src/screens/Tabs/Shared/Toot.tsx
+++ b/src/screens/Tabs/Shared/Toot.tsx
@@ -1,14 +1,20 @@
+import Button from '@components/Button'
import { HeaderLeft } from '@components/Header'
+import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import TimelineDefault from '@components/Timeline/Default'
+import { useQuery } from '@tanstack/react-query'
+import apiGeneral from '@utils/api/general'
+import apiInstance from '@utils/api/instance'
+import { getHost } from '@utils/helpers/urlMatcher'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
-import { QueryKeyTimeline, useTootQuery } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
-import React, { useEffect, useRef } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, View } from 'react-native'
+import { Circle } from 'react-native-animated-spinkit'
import { Path, Svg } from 'react-native-svg'
const TabSharedToot: React.FC> = ({
@@ -18,74 +24,271 @@ const TabSharedToot: React.FC> = ({
}
}) => {
const { colors } = useTheme()
- const { t } = useTranslation('screenTabs')
+ const { t } = useTranslation(['componentTimeline', 'screenTabs'])
+
+ const [hasRemoteContent, setHasRemoteContent] = useState(false)
useEffect(() => {
navigation.setOptions({
- title: t('shared.toot.name'),
+ headerTitle: () => (
+
+ {hasRemoteContent ? (
+
+ ) : null}
+
+
+ ),
headerLeft: () => navigation.goBack()} />
})
- }, [])
+ }, [hasRemoteContent])
const flRef = useRef(null)
const scrolled = useRef(false)
- const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Toot', toot: toot.id }]
- const { data } = useTootQuery({
- ...queryKey[1],
- options: {
- meta: { toot },
+ const finalData = useRef<(Mastodon.Status & { _level?: number; _remote?: boolean })[]>([
+ { ...toot, _level: 0, _remote: false }
+ ])
+ const highlightIndex = useRef(0)
+ const queryLocal = useQuery(
+ ['Timeline', { page: 'Toot', toot: toot.id, remote: false }],
+ async () => {
+ const context = await apiInstance<{
+ ancestors: Mastodon.Status[]
+ descendants: Mastodon.Status[]
+ }>({
+ method: 'get',
+ url: `statuses/${toot.id}/context`
+ })
+
+ const statuses: (Mastodon.Status & { _level?: number })[] = [
+ ...context.body.ancestors,
+ toot,
+ ...context.body.descendants
+ ]
+
+ const highlight = context.body.ancestors.length
+ highlightIndex.current = highlight
+
+ for (const [index, status] of statuses.entries()) {
+ if (index < highlight || status.id === toot.id) {
+ statuses[index]._level = 0
+ continue
+ }
+
+ const repliedLevel = statuses.find(s => s.id === status.in_reply_to_id)?._level
+ statuses[index]._level = (repliedLevel || 0) + 1
+ }
+
+ return { pages: [{ body: statuses }] }
+ },
+ {
+ staleTime: 0,
+ refetchOnMount: true,
onSuccess: data => {
- if (data.body.length < 1) {
+ if (data.pages[0].body.length < 1) {
navigation.goBack()
return
}
- if (!scrolled.current) {
- scrolled.current = true
- const pointer = data.body.findIndex(({ id }) => id === toot.id)
- if (pointer < 1) return
- const length = flRef.current?.props.data?.length
- if (!length) return
- try {
- setTimeout(() => {
- try {
- flRef.current?.scrollToIndex({
- index: pointer,
- viewOffset: 100
- })
- } catch {}
- }, 500)
- } catch (error) {
- return
+ if (finalData.current.length < data.pages[0].body.length) {
+ // if the remote has been loaded first
+ finalData.current = data.pages[0].body
+
+ if (!scrolled.current) {
+ scrolled.current = true
+ const pointer = data.pages[0].body.findIndex(({ id }) => id === toot.id)
+ if (pointer < 1) return
+ const length = flRef.current?.props.data?.length
+ if (!length) return
+ try {
+ setTimeout(() => {
+ try {
+ flRef.current?.scrollToIndex({
+ index: pointer,
+ viewOffset: 100
+ })
+ } catch {}
+ }, 500)
+ } catch (error) {
+ return
+ }
}
}
}
}
- })
+ )
+ useQuery(
+ ['Timeline', { page: 'Toot', toot: toot.id, remote: true }],
+ async () => {
+ let context:
+ | {
+ ancestors: Mastodon.Status[]
+ descendants: Mastodon.Status[]
+ }
+ | undefined
+
+ try {
+ const domain = getHost(toot.url || toot.uri)
+ if (!domain?.length) {
+ throw new Error()
+ }
+ const id = (toot.url || toot.uri).match(new RegExp(/\/([0-9]+)$/))?.[1]
+ if (!id?.length) {
+ throw new Error()
+ }
+
+ context = await apiGeneral<{
+ ancestors: Mastodon.Status[]
+ descendants: Mastodon.Status[]
+ }>({
+ method: 'get',
+ domain,
+ url: `api/v1/statuses/${id}/context`
+ }).then(res => res.body)
+ } catch {}
+
+ if (!context) {
+ throw new Error()
+ }
+
+ const statuses: (Mastodon.Status & { _level?: number })[] = [
+ ...context.ancestors,
+ toot,
+ ...context.descendants
+ ]
+
+ const highlight = context.ancestors.length
+ highlightIndex.current = highlight
+
+ for (const [index, status] of statuses.entries()) {
+ if (index < highlight || status.id === toot.id) {
+ statuses[index]._level = 0
+ continue
+ }
+
+ const repliedLevel = statuses.find(s => s.id === status.in_reply_to_id)?._level
+ statuses[index]._level = (repliedLevel || 0) + 1
+ }
+
+ return { pages: [{ body: statuses }] }
+ },
+ {
+ enabled: toot.account.acct !== toot.account.username, // When on the same instance, these two values are the same
+ staleTime: 0,
+ refetchOnMount: false,
+ onSuccess: data => {
+ if (finalData.current.length < 1 && data.pages[0].body.length < 1) {
+ navigation.goBack()
+ return
+ }
+
+ if (finalData.current?.length < data.pages[0].body.length) {
+ finalData.current = data.pages[0].body.map(remote => {
+ const localMatch = finalData.current?.find(local => local.uri === remote.uri)
+ return localMatch ? { ...localMatch, _remote: false } : { ...remote, _remote: true }
+ })
+ setHasRemoteContent(true)
+ }
+
+ scrolled.current = true
+ const pointer = data.pages[0].body.findIndex(({ id }) => id === toot.id)
+ if (pointer < 1) return
+ const length = flRef.current?.props.data?.length
+ if (!length) return
+ try {
+ setTimeout(() => {
+ try {
+ flRef.current?.scrollToIndex({
+ index: pointer,
+ viewOffset: 100
+ })
+ } catch {}
+ }, 500)
+ } catch (error) {
+ return
+ }
+ }
+ }
+ )
+
+ const empty = () => {
+ switch (queryLocal.status) {
+ case 'error':
+ return (
+ <>
+
+
+ {t('componentTimeline:empty.error.message')}
+
+