From 7ccfdc7562322287945041aad35a0450ec5d2c58 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 31 Dec 2022 02:06:19 +0100 Subject: [PATCH] Fix #631 --- src/components/Parse/HTML.tsx | 60 +++++++++++-------- src/components/Timeline/Default.tsx | 4 +- src/components/Timeline/Shared/Content.tsx | 25 +------- src/components/Timeline/Shared/Context.tsx | 1 + .../Timeline/Shared/HeaderDefault.tsx | 2 + .../Timeline/Shared/HeaderShared/Replies.tsx | 50 ++++++++++++++++ src/utils/helpers/removeHTML.ts | 2 +- 7 files changed, 93 insertions(+), 51 deletions(-) create mode 100644 src/components/Timeline/Shared/HeaderShared/Replies.tsx diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx index 03a4090d..4ff3436f 100644 --- a/src/components/Parse/HTML.tsx +++ b/src/components/Parse/HTML.tsx @@ -1,6 +1,7 @@ import Icon from '@components/Icon' import openLink from '@components/openLink' import ParseEmojis from '@components/Parse/Emojis' +import StatusContext from '@components/Timeline/Shared/Context' import { useNavigation, useRoute } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import { TabLocalStackParamList } from '@utils/navigation/navigators' @@ -11,43 +12,38 @@ import { adaptiveScale } from '@utils/styles/scaling' import { useTheme } from '@utils/styles/ThemeManager' import { ChildNode } from 'domhandler' import { ElementType, parseDocument } from 'htmlparser2' -import React, { useState } from 'react' +import i18next from 'i18next' +import React, { useContext, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Pressable, Text, TextStyleIOS, View } from 'react-native' +import { Platform, Pressable, Text, TextStyleIOS, View } from 'react-native' export interface Props { content: string size?: 'S' | 'M' | 'L' - textStyles?: TextStyleIOS adaptiveSize?: boolean - emojis?: Mastodon.Emoji[] - mentions?: Mastodon.Mention[] - tags?: Mastodon.Tag[] showFullLink?: boolean numberOfLines?: number expandHint?: string - highlighted?: boolean - disableDetails?: boolean selectable?: boolean setSpoilerExpanded?: React.Dispatch> + emojis?: Mastodon.Emoji[] + mentions?: Mastodon.Mention[] } const ParseHTML: React.FC = ({ content, size = 'M', - textStyles, adaptiveSize = false, - emojis, - mentions, - tags, showFullLink = false, numberOfLines = 10, expandHint, - highlighted = false, - disableDetails = false, selectable = false, - setSpoilerExpanded + setSpoilerExpanded, + emojis, + mentions }) => { + const { status, highlighted, disableDetails, excludeMentions } = useContext(StatusContext) + const [adaptiveFontsize] = useGlobalStorage.number('app.font_size') const adaptedFontsize = adaptiveScale( StyleConstants.Font.Size[size], @@ -91,14 +87,16 @@ const ParseHTML: React.FC = ({ return '' } } + const startingOfText = useRef(false) 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 return ( @@ -138,19 +136,28 @@ const ParseHTML: React.FC = ({ /> ) } - if (classes.includes('mention') && mentions?.length) { - const mentionIndex = mentions.findIndex(mention => mention.url === href) + if (classes.includes('mention') && (status?.mentions?.length || mentions?.length)) { + const matchedMention = (status?.mentions || mentions || []).find( + mention => mention.url === href + ) + if ( + matchedMention && + !startingOfText.current && + excludeMentions?.current.find(eM => eM.id === matchedMention.id) + ) { + return null + } const paramsAccount = (params as { account: Mastodon.Account } | undefined)?.account - const sameAccount = paramsAccount?.id === mentions[mentionIndex]?.id + const sameAccount = paramsAccount?.id === matchedMention?.id return ( -1 ? colors.blue : undefined }} + style={{ color: matchedMention ? colors.blue : undefined }} onPress={() => - mentionIndex > -1 && + matchedMention && !disableDetails && !sameAccount && - navigation.push('Tab-Shared-Account', { account: mentions[mentionIndex] }) + navigation.push('Tab-Shared-Account', { account: matchedMention }) } children={node.children.map(unwrapNode).join('')} /> @@ -159,7 +166,7 @@ const ParseHTML: React.FC = ({ } const content = node.children.map(child => unwrapNode(child)).join('') - const shouldBeTag = tags && tags.find(tag => `#${tag.name}` === content) + const shouldBeTag = status?.tags?.find(tag => `#${tag.name}` === content) return ( = ({ style={{ fontSize: adaptedFontsize, lineHeight: adaptedLineheight, - ...textStyles, + ...(Platform.OS === 'ios' && + status?.language && + i18next.dir(status.language) === 'rtl' && + ({ writingDirection: 'rtl' } as { writingDirection: 'rtl' })), height: numberOfLines === 1 && !expanded ? 0 : undefined }} numberOfLines={ diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index 94a5b81d..26c8da51 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -51,7 +51,7 @@ const TimelineDefault: React.FC = ({ }) => { const status = item.reblog ? item.reblog : item const rawContent = useRef([]) - if (highlighted) { + if (highlighted || isConversation) { rawContent.current = [ removeHTML(status.content), status.spoiler_text ? removeHTML(status.spoiler_text) : '' @@ -72,6 +72,7 @@ const TimelineDefault: React.FC = ({ ? !preferences?.['reading:expand:spoilers'] && !spoilerExpanded : false const detectedLanguage = useRef(status.language || '') + const excludeMentions = useRef([]) const mainStyle: StyleProp = { flex: 1, @@ -169,6 +170,7 @@ const TimelineDefault: React.FC = ({ spoilerHidden, rawContent, detectedLanguage, + excludeMentions, highlighted, inThread: queryKey?.[1].page === 'Toot', disableDetails, diff --git a/src/components/Timeline/Shared/Content.tsx b/src/components/Timeline/Shared/Content.tsx index c6003378..13e2f135 100644 --- a/src/components/Timeline/Shared/Content.tsx +++ b/src/components/Timeline/Shared/Content.tsx @@ -3,10 +3,9 @@ import CustomText from '@components/Text' import { usePreferencesQuery } from '@utils/queryHooks/preferences' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import i18next from 'i18next' import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' -import { Platform, View } from 'react-native' +import { View } from 'react-native' import StatusContext from './Context' export interface Props { @@ -23,11 +22,6 @@ const TimelineContent: React.FC = ({ notificationOwnToot = false, setSpoi const { data: preferences } = usePreferencesQuery() - const isRTLiOSTextStyles = - Platform.OS === 'ios' && status.language && i18next.dir(status.language) === 'rtl' - ? ({ writingDirection: 'rtl' } as { writingDirection: 'rtl' }) - : undefined - return ( {status.spoiler_text?.length ? ( @@ -36,13 +30,7 @@ const TimelineContent: React.FC = ({ notificationOwnToot = false, setSpoi content={status.spoiler_text} size={highlighted ? 'L' : 'M'} adaptiveSize - emojis={status.emojis} - mentions={status.mentions} - tags={status.tags} numberOfLines={999} - highlighted={highlighted} - disableDetails={disableDetails} - textStyles={isRTLiOSTextStyles} /> {inThread ? ( = ({ notificationOwnToot = false, setSpoi content={status.content} size={highlighted ? 'L' : 'M'} adaptiveSize - emojis={status.emojis} - mentions={status.mentions} - tags={status.tags} numberOfLines={ preferences?.['reading:expand:spoilers'] || inThread ? notificationOwnToot @@ -72,9 +57,6 @@ const TimelineContent: React.FC = ({ notificationOwnToot = false, setSpoi } expandHint={t('shared.content.expandHint')} setSpoilerExpanded={setSpoilerExpanded} - highlighted={highlighted} - disableDetails={disableDetails} - textStyles={isRTLiOSTextStyles} /> ) : ( @@ -82,12 +64,7 @@ const TimelineContent: React.FC = ({ notificationOwnToot = false, setSpoi content={status.content} size={highlighted ? 'L' : 'M'} adaptiveSize - emojis={status.emojis} - mentions={status.mentions} - tags={status.tags} numberOfLines={highlighted || inThread ? 999 : notificationOwnToot ? 2 : undefined} - disableDetails={disableDetails} - textStyles={isRTLiOSTextStyles} /> )} diff --git a/src/components/Timeline/Shared/Context.tsx b/src/components/Timeline/Shared/Context.tsx index 8917c2fb..80f1079e 100644 --- a/src/components/Timeline/Shared/Context.tsx +++ b/src/components/Timeline/Shared/Context.tsx @@ -14,6 +14,7 @@ type StatusContextType = { spoilerHidden?: boolean rawContent?: React.MutableRefObject // When highlighted, for translate, edit history detectedLanguage?: React.MutableRefObject + excludeMentions?: React.MutableRefObject highlighted?: boolean inThread?: boolean diff --git a/src/components/Timeline/Shared/HeaderDefault.tsx b/src/components/Timeline/Shared/HeaderDefault.tsx index 7fa0545d..c96bf9ef 100644 --- a/src/components/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timeline/Shared/HeaderDefault.tsx @@ -13,6 +13,7 @@ import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedApplication from './HeaderShared/Application' import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedMuted from './HeaderShared/Muted' +import HeaderSharedReplies from './HeaderShared/Replies' import HeaderSharedVisibility from './HeaderShared/Visibility' const TimelineHeaderDefault: React.FC = () => { @@ -64,6 +65,7 @@ const TimelineHeaderDefault: React.FC = () => { /> + diff --git a/src/components/Timeline/Shared/HeaderShared/Replies.tsx b/src/components/Timeline/Shared/HeaderShared/Replies.tsx new file mode 100644 index 00000000..cd7d975e --- /dev/null +++ b/src/components/Timeline/Shared/HeaderShared/Replies.tsx @@ -0,0 +1,50 @@ +import CustomText from '@components/Text' +import { useNavigation } from '@react-navigation/native' +import { StyleConstants } from '@utils/styles/constants' +import { useTheme } from '@utils/styles/ThemeManager' +import React, { Fragment, useContext } from 'react' +import { useTranslation } from 'react-i18next' +import StatusContext from '../Context' + +const HeaderSharedReplies: React.FC = () => { + const { status, rawContent, excludeMentions, isConversation } = useContext(StatusContext) + if (!isConversation) return null + + const navigation = useNavigation() + const { t } = useTranslation('componentTimeline') + const { colors } = useTheme() + + const mentionsBeginning = rawContent?.current?.[0] + .match(new RegExp(/^(?:@\S+\s+)+/))?.[0] + ?.match(new RegExp(/@\S+/, 'g')) + excludeMentions && + (excludeMentions.current = + mentionsBeginning?.length && status?.mentions + ? status.mentions.filter(mention => mentionsBeginning.includes(`@${mention.username}`)) + : []) + + return excludeMentions?.current.length ? ( + + Replies + {excludeMentions.current.map((mention, index) => ( + + {' '} + navigation.push('Tab-Shared-Account', { account: mention })} + /> + + ))} + + ) : null +} + +export default HeaderSharedReplies diff --git a/src/utils/helpers/removeHTML.ts b/src/utils/helpers/removeHTML.ts index 7f1b9832..069e9ae1 100644 --- a/src/utils/helpers/removeHTML.ts +++ b/src/utils/helpers/removeHTML.ts @@ -15,7 +15,7 @@ const removeHTML = (text: string): string => { parser.write(text) parser.end() - return raw + return raw.replace(new RegExp(/\s$/), '') } export default removeHTML