From 34f7218c341835c44283dd9701d170b3d56dcf71 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sun, 25 Dec 2022 17:40:53 +0100 Subject: [PATCH 1/7] Fix React key missing --- src/components/Timeline/Default.tsx | 8 ++++---- src/components/Timeline/Notifications.tsx | 8 ++++---- src/components/Timeline/Shared/HeaderAndroid.tsx | 8 ++++---- src/components/Timeline/Shared/HeaderDefault.tsx | 8 ++++---- .../Timeline/Shared/HeaderNotification.tsx | 8 ++++---- src/screens/Tabs/Me/Root/Collections.tsx | 8 +------- src/screens/Tabs/Shared/Account.tsx | 8 ++++---- src/utils/queryHooks/tags.ts | 13 ++++++++++++- 8 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index b1b2157d..2fb94521 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -17,7 +17,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { useRef, useState } from 'react' +import React, { Fragment, useRef, useState } from 'react' import { Pressable, StyleProp, View, ViewStyle } from 'react-native' import { useSelector } from 'react-redux' import * as ContextMenu from 'zeego/context-menu' @@ -196,8 +196,8 @@ const TimelineDefault: React.FC = ({ - {[mShare, mStatus, mInstance].map(type => ( - <> + {[mShare, mStatus, mInstance].map((type, i) => ( + {type.map((mGroup, index) => ( {mGroup.map(menu => ( @@ -208,7 +208,7 @@ const TimelineDefault: React.FC = ({ ))} ))} - + ))} diff --git a/src/components/Timeline/Notifications.tsx b/src/components/Timeline/Notifications.tsx index 1e2b9bf7..57e783b3 100644 --- a/src/components/Timeline/Notifications.tsx +++ b/src/components/Timeline/Notifications.tsx @@ -16,7 +16,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { useCallback, useRef, useState } from 'react' +import React, { Fragment, useCallback, useRef, useState } from 'react' import { Pressable, View } from 'react-native' import { useSelector } from 'react-redux' import * as ContextMenu from 'zeego/context-menu' @@ -164,8 +164,8 @@ const TimelineNotifications: React.FC = ({ notification, queryKey }) => { - {[mShare, mStatus, mInstance].map(type => ( - <> + {[mShare, mStatus, mInstance].map((type, i) => ( + {type.map((mGroup, index) => ( {mGroup.map(menu => ( @@ -176,7 +176,7 @@ const TimelineNotifications: React.FC = ({ notification, queryKey }) => { ))} ))} - + ))} diff --git a/src/components/Timeline/Shared/HeaderAndroid.tsx b/src/components/Timeline/Shared/HeaderAndroid.tsx index 51306535..016ec0da 100644 --- a/src/components/Timeline/Shared/HeaderAndroid.tsx +++ b/src/components/Timeline/Shared/HeaderAndroid.tsx @@ -4,7 +4,7 @@ import menuStatus from '@components/contextMenu/status' import Icon from '@components/Icon' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { useContext, useState } from 'react' +import React, { Fragment, useContext, useState } from 'react' import { Platform, View } from 'react-native' import * as DropdownMenu from 'zeego/dropdown-menu' import StatusContext from './Context' @@ -53,8 +53,8 @@ const TimelineHeaderAndroid: React.FC = () => { - {[mShare, mAccount, mStatus].map(type => ( - <> + {[mShare, mAccount, mStatus].map((type, i) => ( + {type.map((mGroup, index) => ( {mGroup.map(menu => ( @@ -65,7 +65,7 @@ const TimelineHeaderAndroid: React.FC = () => { ))} ))} - + ))} diff --git a/src/components/Timeline/Shared/HeaderDefault.tsx b/src/components/Timeline/Shared/HeaderDefault.tsx index a7b7e290..7fa0545d 100644 --- a/src/components/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timeline/Shared/HeaderDefault.tsx @@ -4,7 +4,7 @@ import menuStatus from '@components/contextMenu/status' import Icon from '@components/Icon' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { useContext, useState } from 'react' +import React, { Fragment, useContext, useState } from 'react' import { useTranslation } from 'react-i18next' import { Platform, Pressable, View } from 'react-native' import * as DropdownMenu from 'zeego/dropdown-menu' @@ -83,8 +83,8 @@ const TimelineHeaderDefault: React.FC = () => { - {[mShare, mAccount, mStatus].map(type => ( - <> + {[mShare, mAccount, mStatus].map((type, i) => ( + {type.map((mGroup, index) => ( {mGroup.map(menu => ( @@ -95,7 +95,7 @@ const TimelineHeaderDefault: React.FC = () => { ))} ))} - + ))} diff --git a/src/components/Timeline/Shared/HeaderNotification.tsx b/src/components/Timeline/Shared/HeaderNotification.tsx index 80f73edb..42239322 100644 --- a/src/components/Timeline/Shared/HeaderNotification.tsx +++ b/src/components/Timeline/Shared/HeaderNotification.tsx @@ -10,7 +10,7 @@ import { getInstanceUrl } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import * as WebBrowser from 'expo-web-browser' -import React, { useContext, useState } from 'react' +import React, { Fragment, useContext, useState } from 'react' import { useTranslation } from 'react-i18next' import { Platform, Pressable, View } from 'react-native' import { useSelector } from 'react-redux' @@ -90,8 +90,8 @@ const TimelineHeaderNotification: React.FC = ({ notification }) => { - {[mShare, mStatus, mAccount, mInstance].map(type => ( - <> + {[mShare, mStatus, mAccount, mInstance].map((type, i) => ( + {type.map((mGroup, index) => ( {mGroup.map(menu => ( @@ -102,7 +102,7 @@ const TimelineHeaderNotification: React.FC = ({ notification }) => { ))} ))} - + ))} diff --git a/src/screens/Tabs/Me/Root/Collections.tsx b/src/screens/Tabs/Me/Root/Collections.tsx index 7d46433c..891fb9f6 100644 --- a/src/screens/Tabs/Me/Root/Collections.tsx +++ b/src/screens/Tabs/Me/Root/Collections.tsx @@ -4,11 +4,7 @@ import { useAppDispatch } from '@root/store' import { useAnnouncementQuery } from '@utils/queryHooks/announcement' import { useListsQuery } from '@utils/queryHooks/lists' import { useFollowedTagsQuery } from '@utils/queryHooks/tags' -import { - checkInstanceFeature, - getInstanceMePage, - updateInstanceMePage -} from '@utils/slices/instancesSlice' +import { getInstanceMePage, updateInstanceMePage } from '@utils/slices/instancesSlice' import { getInstancePush } from '@utils/slices/instancesSlice' import React from 'react' import { useTranslation } from 'react-i18next' @@ -21,10 +17,8 @@ const Collections: React.FC = () => { const dispatch = useAppDispatch() const mePage = useSelector(getInstanceMePage) - const canFollowTags = useSelector(checkInstanceFeature('follow_tags')) useFollowedTagsQuery({ options: { - enabled: canFollowTags, onSuccess: data => dispatch( updateInstanceMePage({ diff --git a/src/screens/Tabs/Shared/Account.tsx b/src/screens/Tabs/Shared/Account.tsx index 3dc058fb..3690d0ed 100644 --- a/src/screens/Tabs/Shared/Account.tsx +++ b/src/screens/Tabs/Shared/Account.tsx @@ -10,7 +10,7 @@ import { useAccountQuery } from '@utils/queryHooks/account' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { useEffect, useMemo, useState } from 'react' +import React, { Fragment, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Text, View } from 'react-native' import { useSharedValue } from 'react-native-reanimated' @@ -55,8 +55,8 @@ const TabSharedAccount: React.FC - {[mShare, mAccount].map(type => ( - <> + {[mShare, mAccount].map((type, i) => ( + {type.map((mGroup, index) => ( {mGroup.map(menu => ( @@ -67,7 +67,7 @@ const TabSharedAccount: React.FC ))} ))} - + ))} diff --git a/src/utils/queryHooks/tags.ts b/src/utils/queryHooks/tags.ts index 28e61831..b2ec80e6 100644 --- a/src/utils/queryHooks/tags.ts +++ b/src/utils/queryHooks/tags.ts @@ -11,6 +11,8 @@ import { } from '@tanstack/react-query' import { infinitePageParams } from './utils' import { PagedResponse } from '@api/helpers' +import { useSelector } from 'react-redux' +import { checkInstanceFeature } from '@utils/slices/instancesSlice' export type QueryKeyFollowedTags = ['FollowedTags'] const useFollowedTagsQuery = ( @@ -21,14 +23,23 @@ const useFollowedTagsQuery = ( > } | void ) => { + const canFollowTags = useSelector(checkInstanceFeature('follow_tags')) + const queryKey: QueryKeyFollowedTags = ['FollowedTags'] return useInfiniteQuery( queryKey, async ({ pageParam }: QueryFunctionContext) => { const params: { [key: string]: string } = { ...pageParam } - return await apiInstance({ method: 'get', url: `followed_tags`, params }) + return await apiInstance({ + method: 'get', + url: `followed_tags`, + params: { limit: 200, ...params } + }) }, { + enabled: canFollowTags, + staleTime: Infinity, + cacheTime: Infinity, ...params?.options, ...infinitePageParams } From a40f0d9f82ef67b2d02b8154f937d0936f9a225c Mon Sep 17 00:00:00 2001 From: xmflsct Date: Mon, 26 Dec 2022 01:06:33 +0100 Subject: [PATCH 2/7] Fix #620 #606 --- ...ative-htmlview-npm-0.16.0-501f1b89ba.patch | 22 - fastlane/metadata/en-US/release_notes.txt | 3 +- fastlane/metadata/zh-Hans/release_notes.txt | 3 +- package.json | 3 +- src/@types/untyped.d.ts | 2 - src/components/Parse/HTML.tsx | 429 ++++++++---------- src/components/Timeline/Shared/Content.tsx | 6 +- src/helpers/removeHTML.ts | 2 +- src/helpers/urlMatcher.ts | 2 +- yarn.lock | 106 +---- 10 files changed, 215 insertions(+), 363 deletions(-) delete mode 100644 .yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.patch diff --git a/.yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.patch b/.yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.patch deleted file mode 100644 index f42c82e7..00000000 --- a/.yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/HTMLView.js b/HTMLView.js -index 43f8b7eb552d9a44b5feef74ec2ae7d7ddbc2fca..334d144f1fb96067726bca199ff37267c2fa0fb2 100644 ---- a/HTMLView.js -+++ b/HTMLView.js -@@ -1,7 +1,7 @@ - import React, {PureComponent} from 'react'; - import PropTypes from 'prop-types'; - import htmlToElement from './htmlToElement'; --import {Linking, Platform, StyleSheet, View, ViewPropTypes} from 'react-native'; -+import {Linking, Platform, StyleSheet, View} from 'react-native'; - - const boldStyle = {fontWeight: 'bold'}; - const italicStyle = {fontStyle: 'italic'}; -@@ -146,7 +146,7 @@ HtmlView.propTypes = { - renderNode: PropTypes.func, - RootComponent: PropTypes.func, - rootComponentProps: PropTypes.object, -- style: ViewPropTypes.style, -+ style: PropTypes.object, - stylesheet: PropTypes.object, - TextComponent: PropTypes.func, - textComponentProps: PropTypes.object, diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 731c525f..62516967 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,4 +1,5 @@ Enjoy toooting! This version includes following improvements and fixes: - Allowing adding more context of reports - Option to disable autoplay gif -- Hide boosts from users \ No newline at end of file +- Hide boosts from users +- Followed hashtags are underlined \ No newline at end of file diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index af941da8..8225007b 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,4 +1,5 @@ toooting愉快!此版本包括以下改进和修复: - 可添加举报细节 - 新增暂停自动播放gif动画选项 -- 隐藏用户的转嘟 \ No newline at end of file +- 隐藏用户的转嘟 +- 下划线高亮正在关注的话题标签 \ No newline at end of file diff --git a/package.json b/package.json index aba133e8..ab857b27 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "expo-store-review": "^6.0.0", "expo-video-thumbnails": "^7.0.0", "expo-web-browser": "~12.0.0", + "htmlparser2": "^8.0.1", "i18next": "^22.4.6", "linkify-it": "^4.0.1", "lodash": "^4.17.21", @@ -75,7 +76,6 @@ "react-native-feather": "^1.1.2", "react-native-flash-message": "^0.3.1", "react-native-gesture-handler": "~2.8.0", - "react-native-htmlview": "^0.16.0", "react-native-image-picker": "^4.10.3", "react-native-ios-context-menu": "^1.15.1", "react-native-language-detection": "^0.2.2", @@ -120,7 +120,6 @@ "resolutions": { "react-native-fast-image@^8.6.3": "patch:react-native-fast-image@npm%3A8.6.3#./.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch", "expo-av@^13.0.2": "patch:expo-av@npm%3A13.0.2#./.yarn/patches/expo-av-npm-13.0.2-7a651776f1.patch", - "react-native-htmlview@^0.16.0": "patch:react-native-htmlview@npm%3A0.16.0#./.yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.patch", "react-native-share-menu@^6.0.0": "patch:react-native-share-menu@npm%3A6.0.0#./.yarn/patches/react-native-share-menu-npm-6.0.0-f1094c3204.patch", "@types/react-native-share-menu@^5.0.2": "patch:@types/react-native-share-menu@npm%3A5.0.2#./.yarn/patches/@types-react-native-share-menu-npm-5.0.2-373df17ecc.patch" } diff --git a/src/@types/untyped.d.ts b/src/@types/untyped.d.ts index ad27df89..e00bdb1d 100644 --- a/src/@types/untyped.d.ts +++ b/src/@types/untyped.d.ts @@ -1,7 +1,5 @@ declare module 'gl-react-blurhash' -declare module 'htmlparser2-without-node-native' declare module 'react-native-feather' -declare module 'react-native-htmlview' declare module 'react-native-toast-message' declare module 'rtl-detect' diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx index 1bb73532..2c5f29a9 100644 --- a/src/components/Parse/HTML.tsx +++ b/src/components/Parse/HTML.tsx @@ -1,147 +1,23 @@ import Icon from '@components/Icon' import openLink from '@components/openLink' import ParseEmojis from '@components/Parse/Emojis' -import CustomText from '@components/Text' -import { getHost } from '@helpers/urlMatcher' import { useNavigation, useRoute } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import { TabLocalStackParamList } from '@utils/navigation/navigators' +import { useFollowedTagsQuery } from '@utils/queryHooks/tags' import { getSettingsFontsize } from '@utils/slices/settingsSlice' import { StyleConstants } from '@utils/styles/constants' import layoutAnimation from '@utils/styles/layoutAnimation' import { adaptiveScale } from '@utils/styles/scaling' import { useTheme } from '@utils/styles/ThemeManager' +import { ChildNode } from 'domhandler' +import { ElementType, parseDocument } from 'htmlparser2' import { isEqual } from 'lodash' -import React, { useCallback, useState } from 'react' +import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import { Pressable, TextStyleIOS, View } from 'react-native' -import HTMLView from 'react-native-htmlview' +import { Pressable, Text, TextStyleIOS, View } from 'react-native' import { useSelector } from 'react-redux' -// Prevent going to the same hashtag multiple times -const renderNode = ({ - routeParams, - colors, - node, - index, - adaptedFontsize, - adaptedLineheight, - navigation, - mentions, - tags, - showFullLink, - disableDetails -}: { - routeParams?: any - colors: any - node: any - index: number - adaptedFontsize: number - adaptedLineheight: number - navigation: StackNavigationProp - mentions?: Mastodon.Mention[] - tags?: Mastodon.Tag[] - showFullLink: boolean - disableDetails: boolean -}) => { - switch (node.name) { - case 'a': - const classes = node.attribs.class - const href = node.attribs.href - if (classes) { - if (classes.includes('hashtag')) { - const tag = href?.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) - const differentTag = routeParams?.hashtag - ? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2] - : true - return ( - { - !disableDetails && - differentTag && - navigation.push('Tab-Shared-Hashtag', { - hashtag: tag[1] || tag[2] - }) - }} - > - {node.children[0].data} - {node.children[1]?.children[0].data} - - ) - } else if (classes.includes('mention') && mentions) { - const accountIndex = mentions.findIndex(mention => mention.url === href) - const differentAccount = routeParams?.account - ? routeParams.account.id !== mentions[accountIndex]?.id - : true - return ( - { - accountIndex !== -1 && - !disableDetails && - differentAccount && - navigation.push('Tab-Shared-Account', { - account: mentions[accountIndex] - }) - }} - > - {node.children[0].data} - {node.children[1]?.children[0].data} - - ) - } - } else { - const host = getHost(href) - // Need example here - const content = node.children && node.children[0] && node.children[0].data - const shouldBeTag = tags && tags.filter(tag => `#${tag.name}` === content).length > 0 - return ( - { - if (!disableDetails) { - if (shouldBeTag) { - navigation.push('Tab-Shared-Hashtag', { - hashtag: content.substring(1) - }) - } else { - await openLink(href, navigation) - } - } - }} - > - {content && content !== href ? content : showFullLink ? href : host} - {!shouldBeTag ? '/...' : null} - - ) - } - break - case 'p': - if (!node.children.length) { - return // bug when the tag is empty - } - break - } -} - export interface Props { content: string size?: 'S' | 'M' | 'L' @@ -187,8 +63,8 @@ const ParseHTML = React.memo( ) const navigation = useNavigation>() - const route = useRoute() - const { colors, theme } = useTheme() + const { params } = useRoute() + const { colors } = useTheme() const { t } = useTranslation('componentParse') if (!expandHint) { expandHint = t('HTML.defaultHint') @@ -198,116 +74,195 @@ const ParseHTML = React.memo( numberOfLines = 4 } - const renderNodeCallback = useCallback( - (node: any, index: any) => - renderNode({ - routeParams: route.params, - colors, - node, - index, - adaptedFontsize, - adaptedLineheight, - navigation, - mentions, - tags, - showFullLink, - disableDetails - }), - [] - ) - const textComponent = useCallback(({ children }: any) => { - if (children) { - return ( - - ) - } else { - return null + const followedTags = useFollowedTagsQuery() + + const [totalLines, setTotalLines] = useState() + const [expanded, setExpanded] = useState(highlighted) + + const document = parseDocument(content) + const unwrapNode = (node: ChildNode): string => { + switch (node.type) { + case ElementType.Text: + return node.data + case ElementType.Tag: + if (node.name === 'span') { + if (node.attribs.class?.includes('invisible')) return '' + if (node.attribs.class?.includes('ellipsis')) + return node.children.map(child => unwrapNode(child)).join('') + '...' + } + return node.children.map(child => unwrapNode(child)).join('') + default: + return '' } - }, []) - const rootComponent = useCallback( - ({ children }: any) => { - const { t } = useTranslation('componentParse') - - const [totalLines, setTotalLines] = useState() - const [expanded, setExpanded] = useState(highlighted) - - return ( - - {(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? ( - { - layoutAnimation() - setExpanded(!expanded) - if (setSpoilerExpanded) { - setSpoilerExpanded(!expanded) - } - }} - style={{ - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - minHeight: 44, - backgroundColor: colors.backgroundDefault - }} - > - 1 && typeof totalLines === 'number' - ? t('HTML.moreLines', { count: totalLines - numberOfLines }) - : '' - })} - /> - - - ) : null} - { - if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) { - setTotalLines(nativeEvent.lines.length) - } - }} - style={{ - ...textStyles, - height: numberOfLines === 1 && !expanded ? 0 : undefined - }} - numberOfLines={ - typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined - } - selectable={selectable} + } + const renderNode = (node: ChildNode, index: number) => { + switch (node.type) { + case ElementType.Text: + return ( + - - ) - }, - [theme] - ) + ) + case ElementType.Tag: + switch (node.name) { + case 'a': + const classes = node.attribs.class + const href = node.attribs.href + if (classes) { + if (classes.includes('hashtag')) { + const tag = href.match(new RegExp(/\/tags?\/(.*)/, 'i'))?.[1] + const paramsHashtag = (params as { hashtag: Mastodon.Tag['name'] } | undefined) + ?.hashtag + const sameHashtag = paramsHashtag === tag + const isFollowing = followedTags.data?.pages[0]?.body.find(t => t.name === tag) + return ( + + tag?.length && + !disableDetails && + !sameHashtag && + navigation.push('Tab-Shared-Hashtag', { hashtag: tag }) + } + children={node.children.map(unwrapNode).join('')} + /> + ) + } + if (classes.includes('mention') && mentions?.length) { + const mentionIndex = mentions.findIndex(mention => mention.url === href) + const paramsAccount = (params as { account: Mastodon.Account } | undefined) + ?.account + const sameAccount = paramsAccount?.id === mentions[mentionIndex]?.id + return ( + -1 ? colors.blue : undefined }} + onPress={() => + mentionIndex > -1 && + !disableDetails && + !sameAccount && + navigation.push('Tab-Shared-Account', { account: mentions[mentionIndex] }) + } + children={node.children.map(unwrapNode).join('')} + /> + ) + } + } + const content = node.children.map(child => unwrapNode(child)).join('') + const shouldBeTag = tags && tags.find(tag => `#${tag.name}` === content) + return ( + { + if (!disableDetails) { + if (shouldBeTag) { + navigation.push('Tab-Shared-Hashtag', { + hashtag: content.substring(1) + }) + } else { + await openLink(href, navigation) + } + } + }} + children={content !== href ? content : showFullLink ? href : content} + /> + ) + break + case 'p': + if (index < document.children.length - 1) { + return ( + + {node.children.map((c, i) => renderNode(c, i))} + {'\n\n'} + + ) + } else { + return renderNode(c, i))} /> + } + default: + return renderNode(c, i))} /> + } + } + return null + } return ( - + + {(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? ( + { + layoutAnimation() + setExpanded(!expanded) + if (setSpoilerExpanded) { + setSpoilerExpanded(!expanded) + } + }} + style={{ + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + minHeight: 44, + backgroundColor: colors.backgroundDefault + }} + > + 1 && typeof totalLines === 'number' + ? t('HTML.moreLines', { count: totalLines - numberOfLines }) + : '' + })} + /> + + + ) : null} + { + if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) { + setTotalLines(nativeEvent.lines.length) + } + }} + style={{ + fontSize: adaptedFontsize, + lineHeight: adaptedLineheight, + ...textStyles, + height: numberOfLines === 1 && !expanded ? 0 : undefined + }} + numberOfLines={ + typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined + } + selectable={selectable} + /> + ) }, (prev, next) => prev.content === next.content && isEqual(prev.emojis, next.emojis) diff --git a/src/components/Timeline/Shared/Content.tsx b/src/components/Timeline/Shared/Content.tsx index b250d56a..ef09c945 100644 --- a/src/components/Timeline/Shared/Content.tsx +++ b/src/components/Timeline/Shared/Content.tsx @@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' -import { Platform } from 'react-native' +import { Platform, View } from 'react-native' import { useSelector } from 'react-redux' import { isRtlLang } from 'rtl-detect' import StatusContext from './Context' @@ -24,7 +24,7 @@ const TimelineContent: React.FC = ({ notificationOwnToot = false, setSpoi const instanceAccount = useSelector(getInstanceAccount, () => true) return ( - <> + {status.spoiler_text?.length ? ( <> = ({ notificationOwnToot = false, setSpoi } /> )} - + ) } diff --git a/src/helpers/removeHTML.ts b/src/helpers/removeHTML.ts index e8a3d4c1..7f1b9832 100644 --- a/src/helpers/removeHTML.ts +++ b/src/helpers/removeHTML.ts @@ -1,4 +1,4 @@ -import htmlparser2 from 'htmlparser2-without-node-native' +import * as htmlparser2 from 'htmlparser2' const removeHTML = (text: string): string => { let raw: string = '' diff --git a/src/helpers/urlMatcher.ts b/src/helpers/urlMatcher.ts index c08f036e..8c2286d9 100644 --- a/src/helpers/urlMatcher.ts +++ b/src/helpers/urlMatcher.ts @@ -1,7 +1,7 @@ import { store } from '@root/store' import { getInstanceUrl } from '@utils/slices/instancesSlice' -const getHost = (url: unknown): string | void => { +const getHost = (url: unknown): string | undefined | null => { if (typeof url !== 'string') return undefined const matches = url.match(/^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/\n]+)/i) diff --git a/yarn.lock b/yarn.lock index f555d518..284740c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5210,16 +5210,6 @@ __metadata: languageName: node linkType: hard -"dom-serializer@npm:0": - version: 0.2.2 - resolution: "dom-serializer@npm:0.2.2" - dependencies: - domelementtype: ^2.0.1 - entities: ^2.0.0 - checksum: 376344893e4feccab649a14ca1a46473e9961f40fe62479ea692d4fee4d9df1c00ca8654811a79c1ca7b020096987e1ca4fb4d7f8bae32c1db800a680a0e5d5e - languageName: node - linkType: hard - "dom-serializer@npm:^2.0.0": version: 2.0.0 resolution: "dom-serializer@npm:2.0.0" @@ -5231,29 +5221,13 @@ __metadata: languageName: node linkType: hard -"domelementtype@npm:1, domelementtype@npm:^1.3.0": - version: 1.3.1 - resolution: "domelementtype@npm:1.3.1" - checksum: 7893da40218ae2106ec6ffc146b17f203487a52f5228b032ea7aa470e41dfe03e1bd762d0ee0139e792195efda765434b04b43cddcf63207b098f6ae44b36ad6 - languageName: node - linkType: hard - -"domelementtype@npm:^2.0.1, domelementtype@npm:^2.3.0": +"domelementtype@npm:^2.3.0": version: 2.3.0 resolution: "domelementtype@npm:2.3.0" checksum: ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 languageName: node linkType: hard -"domhandler@npm:^2.3.0": - version: 2.4.2 - resolution: "domhandler@npm:2.4.2" - dependencies: - domelementtype: 1 - checksum: 49bd70c9c784f845cd047e1dfb3611bd10891c05719acfc93f01fc726a419ed09fbe0b69f9064392d556a63fffc5a02010856cedae9368f4817146d95a97011f - languageName: node - linkType: hard - "domhandler@npm:^5.0.1, domhandler@npm:^5.0.2": version: 5.0.3 resolution: "domhandler@npm:5.0.3" @@ -5263,16 +5237,6 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^1.5.1": - version: 1.7.0 - resolution: "domutils@npm:1.7.0" - dependencies: - dom-serializer: 0 - domelementtype: 1 - checksum: f60a725b1f73c1ae82f4894b691601ecc6ecb68320d87923ac3633137627c7865725af813ae5d188ad3954283853bcf46779eb50304ec5d5354044569fcefd2b - languageName: node - linkType: hard - "domutils@npm:^3.0.1": version: 3.0.1 resolution: "domutils@npm:3.0.1" @@ -5337,21 +5301,7 @@ __metadata: languageName: node linkType: hard -"entities@npm:^1.1.1": - version: 1.1.2 - resolution: "entities@npm:1.1.2" - checksum: d537b02799bdd4784ffd714d000597ed168727bddf4885da887c5a491d735739029a00794f1998abbf35f3f6aeda32ef5c15010dca1817d401903a501b6d3e05 - languageName: node - linkType: hard - -"entities@npm:^2.0.0": - version: 2.2.0 - resolution: "entities@npm:2.2.0" - checksum: 19010dacaf0912c895ea262b4f6128574f9ccf8d4b3b65c7e8334ad0079b3706376360e28d8843ff50a78aabcb8f08f0a32dbfacdc77e47ed77ca08b713669b3 - languageName: node - linkType: hard - -"entities@npm:^4.2.0": +"entities@npm:^4.2.0, entities@npm:^4.3.0": version: 4.4.0 resolution: "entities@npm:4.4.0" checksum: 84d250329f4b56b40fa93ed067b194db21e8815e4eb9b59f43a086f0ecd342814f6bc483de8a77da5d64e0f626033192b1b4f1792232a7ea6b970ebe0f3187c2 @@ -5489,13 +5439,6 @@ __metadata: languageName: node linkType: hard -"eventemitter2@npm:^1.0.0": - version: 1.0.5 - resolution: "eventemitter2@npm:1.0.5" - checksum: f501d8ad439aad85b4d5494d96a83abbb156ed4acf5897fed53a99f4fa427c5a968959276461d8640b613ed16a34995fcd63a13404262e725131a29df9177c96 - languageName: node - linkType: hard - "exec-async@npm:^2.2.0": version: 2.2.0 resolution: "exec-async@npm:2.2.0" @@ -6609,18 +6552,15 @@ __metadata: languageName: node linkType: hard -"htmlparser2-without-node-native@npm:^3.9.2": - version: 3.9.2 - resolution: "htmlparser2-without-node-native@npm:3.9.2" +"htmlparser2@npm:^8.0.1": + version: 8.0.1 + resolution: "htmlparser2@npm:8.0.1" dependencies: - domelementtype: ^1.3.0 - domhandler: ^2.3.0 - domutils: ^1.5.1 - entities: ^1.1.1 - eventemitter2: ^1.0.0 - inherits: ^2.0.1 - readable-stream: ^2.0.2 - checksum: 95754dc0f9ab057b5c072eaf2daed6db7595ec7a51290fde0cde6df803cbe528927764da4acb7bc9e633ff6396b07046eff7edb6f1f619fe665df4eaa8c735ad + domelementtype: ^2.3.0 + domhandler: ^5.0.2 + domutils: ^3.0.1 + entities: ^4.3.0 + checksum: 06d5c71e8313597722bc429ae2a7a8333d77bd3ab07ccb916628384b37332027b047f8619448d8f4a3312b6609c6ea3302a4e77435d859e9e686999e6699ca39 languageName: node linkType: hard @@ -6792,7 +6732,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 @@ -9633,26 +9573,6 @@ __metadata: languageName: node linkType: hard -"react-native-htmlview@npm:0.16.0": - version: 0.16.0 - resolution: "react-native-htmlview@npm:0.16.0" - dependencies: - entities: ^1.1.1 - htmlparser2-without-node-native: ^3.9.2 - checksum: fb46bc162e41ba9c1222f9386215496f9ac7d4f7f3353b2732101abc57f89d570e9e0881c581bec04d77c2aeaac916b4bd157eedc2098ea2e94b7a29c5c739b1 - languageName: node - linkType: hard - -"react-native-htmlview@patch:react-native-htmlview@npm%3A0.16.0#./.yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.patch::locator=tooot%40workspace%3A.": - version: 0.16.0 - resolution: "react-native-htmlview@patch:react-native-htmlview@npm%3A0.16.0#./.yarn/patches/react-native-htmlview-npm-0.16.0-501f1b89ba.patch::version=0.16.0&hash=aa9ca8&locator=tooot%40workspace%3A." - dependencies: - entities: ^1.1.1 - htmlparser2-without-node-native: ^3.9.2 - checksum: 87e79231315bedf821bc30d342d5bc70ae6fe73b34d535051aba3a5142a40675a9687b37333968194aa9e28a5bdc476f6e12ea921f207ac4b80396615696e81c - languageName: node - linkType: hard - "react-native-image-picker@npm:^4.10.3": version: 4.10.3 resolution: "react-native-image-picker@npm:4.10.3" @@ -9974,7 +9894,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.6, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.6, readable-stream@npm:~2.3.6": version: 2.3.7 resolution: "readable-stream@npm:2.3.7" dependencies: @@ -11397,6 +11317,7 @@ __metadata: expo-store-review: ^6.0.0 expo-video-thumbnails: ^7.0.0 expo-web-browser: ~12.0.0 + htmlparser2: ^8.0.1 i18next: ^22.4.6 linkify-it: ^4.0.1 lodash: ^4.17.21 @@ -11413,7 +11334,6 @@ __metadata: react-native-feather: ^1.1.2 react-native-flash-message: ^0.3.1 react-native-gesture-handler: ~2.8.0 - react-native-htmlview: ^0.16.0 react-native-image-picker: ^4.10.3 react-native-ios-context-menu: ^1.15.1 react-native-language-detection: ^0.2.2 From 71ccb4a93c6322812b395cbfa01b11dbe465c97e Mon Sep 17 00:00:00 2001 From: John HU Date: Tue, 27 Dec 2022 16:57:35 -0800 Subject: [PATCH 3/7] Fix README.md build status badges (#622) * Fix build status badges Changes required for badges: https://github.com/badges/shields/issues/8671 * Make special thanks section visually compact --- README.md | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 7d29999d..34d1f97c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![GPL-3.0](https://img.shields.io/github/license/tooot-app/push)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot) -![GitHub Workflow Status (candidate)](https://img.shields.io/github/workflow/status/tooot-app/app/build/candidate?label=build%20candidate) ![GitHub Workflow Status (release)](https://img.shields.io/github/workflow/status/tooot-app/app/build/release?label=build%20release) +![GitHub Workflow Status (candidate)](https://img.shields.io/github/actions/workflow/status/tooot-app/app/build.yml?branch=candidate&label=build%20candidate) ![GitHub Workflow Status (release)](https://img.shields.io/github/actions/workflow/status/tooot-app/app/build.yml?branch=release&label=build%20release) ## Contribute to translation @@ -11,28 +11,16 @@ Please **do not** create a pull request to update translation. tooot's translati ## Special thanks -[@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation - -[@forenta](https://github.com/forenta) for German translation - -[@pat](https://piaille.fr/@pat) for French translation - -[@andrigamerita](https://github.com/andrigamerita) for Italian translation - -[@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation - -[@hellojaccc](https://github.com/hellojaccc) for Korean translation - -[@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation - -[@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese - -[@janlindblom](https://github.com/janlindblom) for Swedish - -[@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian - -[@duy@mas.to](https://mas.to/@duy) for Vietnamese translation - -[@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation - -[@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound +- [@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation +- [@forenta](https://github.com/forenta) for German translation +- [@pat](https://piaille.fr/@pat) for French translation +- [@andrigamerita](https://github.com/andrigamerita) for Italian translation +- [@Hikaru](https://github.com/Hikali-47041) and [@la_la](https://mstdn.jp/@la_la_la) for Japanese translation +- [@hellojaccc](https://github.com/hellojaccc) for Korean translation +- [@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation +- [@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese +- [@janlindblom](https://github.com/janlindblom) for Swedish +- [@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian +- [@duy@mas.to](https://mas.to/@duy) for Vietnamese translation +- [@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation +- [@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound From 1ea6aff328c0f5cdbce744a44f442581c15a7a34 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Wed, 28 Dec 2022 23:41:36 +0100 Subject: [PATCH 4/7] 619 restructure local storage (#628) * To MMKV migration working * POC migrated font size settings * Moved settings to mmkv * Fix typos * Migrated contexts slice * Migrated app slice * POC instance emoji update * Migrated drafts * Migrated simple instance properties * All migrated! * Re-structure files * Tolerant of undefined settings * Can properly logging in and out including empty state --- babel.config.js | 5 +- index.js | 2 +- ios/Podfile.lock | 14 + package.json | 3 +- src/@types/mastodon.d.ts | 56 ++- src/App.tsx | 181 ++++---- src/components/AccountButton.tsx | 24 +- src/components/Emojis/Button.tsx | 2 +- .../EmojisContext.tsx => Context.tsx} | 0 src/components/Emojis/List.tsx | 61 ++- .../{Emojis.tsx => Emojis/index.tsx} | 7 +- src/components/GracefullyImage.tsx | 2 +- .../{Header.tsx => Header/index.tsx} | 2 +- src/components/Input.tsx | 2 +- src/components/Instance/Info.tsx | 55 --- src/components/Instance/index.tsx | 156 ++++--- src/components/Menu/Container.tsx | 2 +- src/components/Menu/Header.tsx | 6 +- src/components/{Menu.tsx => Menu/index.tsx} | 0 src/components/Parse.tsx | 4 - src/components/Parse/Emojis.tsx | 5 +- src/components/Parse/HTML.tsx | 5 +- src/components/Parse/index.tsx | 4 + src/components/Relationship/Incoming.tsx | 2 +- src/components/Relationship/Outgoing.tsx | 11 +- .../index.tsx} | 0 src/components/Timeline/Conversation.tsx | 4 +- src/components/Timeline/Default.tsx | 18 +- src/components/Timeline/Notifications.tsx | 18 +- src/components/Timeline/Refresh.tsx | 2 +- src/components/Timeline/Shared/Actions.tsx | 11 +- src/components/Timeline/Shared/Attachment.tsx | 12 +- .../Timeline/Shared/Attachment/Video.tsx | 9 +- src/components/Timeline/Shared/Card.tsx | 2 +- src/components/Timeline/Shared/Content.tsx | 8 +- src/components/Timeline/Shared/Filtered.tsx | 10 +- .../Timeline/Shared/HeaderConversation.tsx | 2 +- .../Timeline/Shared/HeaderNotification.tsx | 11 +- .../Timeline/Shared/HeaderShared/Account.tsx | 2 +- src/components/Timeline/Shared/Poll.tsx | 3 +- src/components/Timeline/Shared/Translate.tsx | 4 +- .../{Timeline.tsx => Timeline/index.tsx} | 18 +- src/components/contextMenu/account.ts | 8 +- src/components/contextMenu/instance.ts | 11 +- src/components/contextMenu/status.ts | 18 +- src/components/mediaSelector.ts | 11 +- src/components/openLink.ts | 13 +- src/helpers/getLanguage.ts | 12 - src/i18n/{i18n.ts => index.ts} | 32 +- src/screens/AccountSelection.tsx | 44 +- src/screens/Compose/DraftsList.tsx | 26 +- src/screens/Compose/EditAttachment.tsx | 2 +- src/screens/Compose/Posting.tsx | 2 +- src/screens/Compose/Root/Actions.tsx | 18 +- src/screens/Compose/Root/Drafts.tsx | 14 +- .../Compose/Root/Footer/Attachments.tsx | 7 +- src/screens/Compose/Root/Footer/Poll.tsx | 15 +- .../Compose/Root/Footer/addAttachment.ts | 10 +- .../Root/{Footer.tsx => Footer/index.tsx} | 0 src/screens/Compose/Root/Header.tsx | 36 -- src/screens/Compose/Root/Header/PostingAs.tsx | 30 +- .../Compose/Root/Header/SpoilerInput.tsx | 5 +- src/screens/Compose/Root/Header/TextInput.tsx | 11 +- src/screens/Compose/Root/Header/index.tsx | 20 + .../Compose/{Root.tsx => Root/index.tsx} | 23 +- .../{Compose.tsx => Compose/index.tsx} | 112 ++--- src/screens/Compose/utils/parseState.ts | 10 +- src/screens/Compose/utils/post.ts | 4 +- src/screens/Compose/utils/processText.tsx | 15 +- src/screens/Compose/utils/types.d.ts | 96 ++--- .../index.tsx} | 6 +- src/screens/Tabs/Local/Root.tsx | 48 +-- src/screens/Tabs/Me/List/Edit.tsx | 8 +- src/screens/Tabs/Me/List/index.tsx | 2 +- src/screens/Tabs/Me/List/menus.tsx | 4 +- src/screens/Tabs/Me/Profile/Fields.tsx | 2 +- src/screens/Tabs/Me/Profile/Name.tsx | 2 +- src/screens/Tabs/Me/Profile/Note.tsx | 2 +- src/screens/Tabs/Me/Profile/Root.tsx | 16 +- .../Me/{Profile.tsx => Profile/index.tsx} | 10 +- src/screens/Tabs/Me/Push.tsx | 205 ++++++--- src/screens/Tabs/Me/Root/Collections.tsx | 48 +-- src/screens/Tabs/Me/Root/Logout.tsx | 20 +- src/screens/Tabs/Me/Root/Settings.tsx | 13 +- .../Tabs/Me/{Root.tsx => Root/index.tsx} | 17 +- src/screens/Tabs/Me/Settings/Analytics.tsx | 7 +- src/screens/Tabs/Me/Settings/App.tsx | 73 ++-- src/screens/Tabs/Me/Settings/Dev.tsx | 66 ++- src/screens/Tabs/Me/Settings/Tooot.tsx | 19 +- .../Me/{Settings.tsx => Settings/index.tsx} | 10 +- src/screens/Tabs/Me/SettingsFontsize.tsx | 80 ++-- src/screens/Tabs/Me/SettingsLanguage.tsx | 17 +- src/screens/Tabs/Me/Switch.tsx | 52 +-- src/screens/Tabs/Me/index.tsx | 2 +- src/screens/Tabs/Notifications/Filters.tsx | 24 +- src/screens/Tabs/Public/Root.tsx | 13 +- src/screens/Tabs/Shared/Account/Header.tsx | 7 +- .../Tabs/Shared/Account/Information.tsx | 4 +- .../Shared/Account/Information/Account.tsx | 21 +- .../Shared/Account/Information/Actions.tsx | 7 +- .../Shared/Account/Information/Avatar.tsx | 9 +- .../Tabs/Shared/Account/Information/Stats.tsx | 4 +- .../Shared/{Account.tsx => Account/index.tsx} | 8 +- src/screens/Tabs/Shared/Hashtag.tsx | 5 +- src/screens/Tabs/Shared/History.tsx | 2 +- src/screens/Tabs/Shared/Report.tsx | 8 +- src/screens/Tabs/Shared/Toot.tsx | 10 +- src/screens/Tabs/Shared/Users.tsx | 2 +- src/screens/{Tabs.tsx => Tabs/index.tsx} | 28 +- src/{Screens.tsx => screens/index.tsx} | 68 ++- src/store.ts | 94 ----- .../accessibility/AccessibilityManager.tsx | 10 +- src/{ => utils}/api/general.ts | 0 src/{ => utils}/api/helpers/index.ts | 1 + src/{ => utils}/api/instance.ts | 36 +- src/{ => utils}/api/tooot.ts | 2 +- .../helpers/androidActionSheetStyles.ts | 0 src/{ => utils}/helpers/browserPackage.ts | 0 src/utils/{ => helpers}/checkEnvironment.ts | 0 src/{ => utils}/helpers/detectLanguage.ts | 2 +- src/utils/helpers/featureCheck.ts | 58 +++ src/{ => utils}/helpers/features.json | 4 + src/utils/helpers/getLanguage.ts | 9 + src/{ => utils}/helpers/permissions.ts | 0 src/{ => utils}/helpers/removeHTML.ts | 0 src/{ => utils}/helpers/urlMatcher.ts | 13 +- src/utils/initQuery.ts | 11 - src/utils/migrations/app/v0.ts | 4 - src/utils/migrations/contexts/migration.ts | 27 -- src/utils/migrations/contexts/v0.ts | 13 - src/utils/migrations/contexts/v1.ts | 17 - src/utils/migrations/contexts/v2.ts | 13 - src/utils/migrations/contexts/v3.ts | 19 - src/utils/migrations/instances/migration.ts | 172 -------- src/utils/migrations/instances/v10.ts | 89 ---- src/utils/migrations/instances/v11.ts | 60 --- src/utils/migrations/instances/v3.ts | 33 -- src/utils/migrations/instances/v4.ts | 84 ---- src/utils/migrations/instances/v5.ts | 93 ----- src/utils/migrations/instances/v6.ts | 66 --- src/utils/migrations/instances/v7.ts | 77 ---- src/utils/migrations/instances/v8.ts | 83 ---- src/utils/migrations/instances/v9.ts | 79 ---- src/utils/migrations/settings/migration.ts | 33 -- src/utils/migrations/settings/v0.ts | 7 - src/utils/migrations/settings/v1.ts | 8 - src/utils/migrations/settings/v2.ts | 9 - src/utils/migrations/settings/v3.ts | 8 - src/utils/migrations/settings/v4.ts | 8 - .../navigation}/navigationRef.ts | 0 src/utils/navigation/usePopToTop.ts | 11 +- src/utils/push/constants.ts | 94 +++++ src/utils/push/updateExpoToken.ts | 27 ++ src/utils/push/useConnect.ts | 50 ++- src/utils/push/useNavigate.ts | 4 +- src/utils/push/useReceive.ts | 21 +- src/utils/push/useRespond.ts | 22 +- src/utils/queryHooks/account.ts | 4 +- src/utils/queryHooks/announcement.ts | 14 +- src/utils/queryHooks/apps.ts | 18 +- src/utils/queryHooks/emojis.ts | 34 +- src/utils/queryHooks/filters.ts | 24 ++ .../queryHooks/index.ts} | 0 src/utils/queryHooks/instance.ts | 84 ++-- src/utils/queryHooks/lists.ts | 20 +- src/utils/queryHooks/preferences.ts | 27 ++ src/utils/queryHooks/profile.ts | 6 +- src/utils/queryHooks/relationship.ts | 14 +- src/utils/queryHooks/reports.ts | 4 +- src/utils/queryHooks/search.ts | 4 +- src/utils/queryHooks/status.ts | 4 +- src/utils/queryHooks/statusesHistory.ts | 4 +- src/utils/queryHooks/tags.ts | 11 +- src/utils/queryHooks/timeline.ts | 33 +- src/utils/queryHooks/timeline/deleteItem.ts | 2 +- src/utils/queryHooks/timeline/editItem.ts | 2 +- .../timeline/updateStatusProperty.ts | 2 +- src/utils/queryHooks/translate.ts | 4 +- src/utils/queryHooks/trends.ts | 9 +- src/utils/queryHooks/users.ts | 18 +- src/utils/queryHooks/utils.ts | 2 +- src/utils/slices/appSlice.ts | 58 --- src/utils/slices/contextsSlice.ts | 62 --- src/utils/slices/instances/add.ts | 133 ------ src/utils/slices/instances/checkEmojis.ts | 15 - src/utils/slices/instances/push/register.ts | 106 ----- src/utils/slices/instances/push/unregister.ts | 35 -- src/utils/slices/instances/push/utils.ts | 120 ------ src/utils/slices/instances/remove.ts | 38 -- .../instances/updateAccountPreferences.ts | 20 - .../slices/instances/updateConfiguration.ts | 12 - src/utils/slices/instances/updateFilters.ts | 12 - src/utils/slices/instances/updatePush.ts | 25 -- src/utils/slices/instances/updatePushAlert.ts | 25 -- .../slices/instances/updatePushDecode.ts | 40 -- src/utils/slices/instancesSlice.ts | 390 ------------------ src/utils/slices/settingsSlice.ts | 65 --- src/{ => utils}/startup/audio.ts | 0 src/{ => utils}/startup/log.ts | 0 src/{ => utils}/startup/netInfo.ts | 29 +- src/{ => utils}/startup/push.ts | 0 src/{ => utils}/startup/sentry.ts | 2 +- src/{ => utils}/startup/timezone.ts | 0 src/utils/storage/account/index.ts | 3 + src/utils/storage/account/v0.ts | 60 +++ src/utils/storage/actions.ts | 228 ++++++++++ src/utils/storage/global/index.ts | 3 + src/utils/storage/global/v0.ts | 24 ++ src/utils/storage/index.ts | 6 + src/utils/storage/migrations/toMMKV.ts | 114 +++++ src/utils/styles/ThemeManager.tsx | 27 +- src/utils/styles/themes.ts | 2 +- tsconfig.json | 6 +- yarn.lock | 71 +--- 214 files changed, 2151 insertions(+), 3694 deletions(-) rename src/components/Emojis/{helpers/EmojisContext.tsx => Context.tsx} (100%) rename src/components/{Emojis.tsx => Emojis/index.tsx} (94%) rename src/components/{Header.tsx => Header/index.tsx} (100%) delete mode 100644 src/components/Instance/Info.tsx rename src/components/{Menu.tsx => Menu/index.tsx} (100%) delete mode 100644 src/components/Parse.tsx create mode 100644 src/components/Parse/index.tsx rename src/components/{Relationship.tsx => Relationship/index.tsx} (100%) rename src/components/{Timeline.tsx => Timeline/index.tsx} (91%) delete mode 100644 src/helpers/getLanguage.ts rename src/i18n/{i18n.ts => index.ts} (89%) rename src/screens/Compose/Root/{Footer.tsx => Footer/index.tsx} (100%) delete mode 100644 src/screens/Compose/Root/Header.tsx create mode 100644 src/screens/Compose/Root/Header/index.tsx rename src/screens/Compose/{Root.tsx => Root/index.tsx} (80%) rename src/screens/{Compose.tsx => Compose/index.tsx} (82%) rename src/screens/{ImagesViewer.tsx => ImageViewer/index.tsx} (97%) rename src/screens/Tabs/Me/{Profile.tsx => Profile/index.tsx} (90%) rename src/screens/Tabs/Me/{Root.tsx => Root/index.tsx} (73%) rename src/screens/Tabs/Me/{Settings.tsx => Settings/index.tsx} (58%) rename src/screens/Tabs/Shared/{Account.tsx => Account/index.tsx} (97%) rename src/screens/{Tabs.tsx => Tabs/index.tsx} (78%) rename src/{Screens.tsx => screens/index.tsx} (83%) delete mode 100644 src/store.ts rename src/{ => utils}/api/general.ts (100%) rename src/{ => utils}/api/helpers/index.ts (98%) rename src/{ => utils}/api/instance.ts (69%) rename src/{ => utils}/api/tooot.ts (96%) rename src/{ => utils}/helpers/androidActionSheetStyles.ts (100%) rename src/{ => utils}/helpers/browserPackage.ts (100%) rename src/utils/{ => helpers}/checkEnvironment.ts (100%) rename src/{ => utils}/helpers/detectLanguage.ts (84%) create mode 100644 src/utils/helpers/featureCheck.ts rename src/{ => utils}/helpers/features.json (92%) create mode 100644 src/utils/helpers/getLanguage.ts rename src/{ => utils}/helpers/permissions.ts (100%) rename src/{ => utils}/helpers/removeHTML.ts (100%) rename src/{ => utils}/helpers/urlMatcher.ts (79%) delete mode 100644 src/utils/initQuery.ts delete mode 100644 src/utils/migrations/app/v0.ts delete mode 100644 src/utils/migrations/contexts/migration.ts delete mode 100644 src/utils/migrations/contexts/v0.ts delete mode 100644 src/utils/migrations/contexts/v1.ts delete mode 100644 src/utils/migrations/contexts/v2.ts delete mode 100644 src/utils/migrations/contexts/v3.ts delete mode 100644 src/utils/migrations/instances/migration.ts delete mode 100644 src/utils/migrations/instances/v10.ts delete mode 100644 src/utils/migrations/instances/v11.ts delete mode 100644 src/utils/migrations/instances/v3.ts delete mode 100644 src/utils/migrations/instances/v4.ts delete mode 100644 src/utils/migrations/instances/v5.ts delete mode 100644 src/utils/migrations/instances/v6.ts delete mode 100644 src/utils/migrations/instances/v7.ts delete mode 100644 src/utils/migrations/instances/v8.ts delete mode 100644 src/utils/migrations/instances/v9.ts delete mode 100644 src/utils/migrations/settings/migration.ts delete mode 100644 src/utils/migrations/settings/v0.ts delete mode 100644 src/utils/migrations/settings/v1.ts delete mode 100644 src/utils/migrations/settings/v2.ts delete mode 100644 src/utils/migrations/settings/v3.ts delete mode 100644 src/utils/migrations/settings/v4.ts rename src/{helpers => utils/navigation}/navigationRef.ts (100%) create mode 100644 src/utils/push/constants.ts create mode 100644 src/utils/push/updateExpoToken.ts create mode 100644 src/utils/queryHooks/filters.ts rename src/{helpers/queryClient.ts => utils/queryHooks/index.ts} (100%) create mode 100644 src/utils/queryHooks/preferences.ts delete mode 100644 src/utils/slices/appSlice.ts delete mode 100644 src/utils/slices/contextsSlice.ts delete mode 100644 src/utils/slices/instances/add.ts delete mode 100644 src/utils/slices/instances/checkEmojis.ts delete mode 100644 src/utils/slices/instances/push/register.ts delete mode 100644 src/utils/slices/instances/push/unregister.ts delete mode 100644 src/utils/slices/instances/push/utils.ts delete mode 100644 src/utils/slices/instances/remove.ts delete mode 100644 src/utils/slices/instances/updateAccountPreferences.ts delete mode 100644 src/utils/slices/instances/updateConfiguration.ts delete mode 100644 src/utils/slices/instances/updateFilters.ts delete mode 100644 src/utils/slices/instances/updatePush.ts delete mode 100644 src/utils/slices/instances/updatePushAlert.ts delete mode 100644 src/utils/slices/instances/updatePushDecode.ts delete mode 100644 src/utils/slices/instancesSlice.ts delete mode 100644 src/utils/slices/settingsSlice.ts rename src/{ => utils}/startup/audio.ts (100%) rename src/{ => utils}/startup/log.ts (100%) rename src/{ => utils}/startup/netInfo.ts (70%) rename src/{ => utils}/startup/push.ts (100%) rename src/{ => utils}/startup/sentry.ts (90%) rename src/{ => utils}/startup/timezone.ts (100%) create mode 100644 src/utils/storage/account/index.ts create mode 100644 src/utils/storage/account/v0.ts create mode 100644 src/utils/storage/actions.ts create mode 100644 src/utils/storage/global/index.ts create mode 100644 src/utils/storage/global/v0.ts create mode 100644 src/utils/storage/index.ts create mode 100644 src/utils/storage/migrations/toMMKV.ts diff --git a/babel.config.js b/babel.config.js index 1ffee43c..0d5803fa 100644 --- a/babel.config.js +++ b/babel.config.js @@ -8,11 +8,8 @@ module.exports = function (api) { { root: ['./'], alias: { - '@assets': './assets', - '@root': './src', - '@api': './src/api', - '@helpers': './src/helpers', '@components': './src/components', + '@i18n': './src/i18n', '@screens': './src/screens', '@utils': './src/utils' } diff --git a/index.js b/index.js index 40a9d636..d292a7a1 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ import { registerRootComponent } from 'expo' -import App from '@root/App' +import App from './src/App' // registerRootComponent calls AppRegistry.registerComponent('main', () => App); // It also ensures that whether you load the app in the Expo client or in a native build, diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e148c49d..478da943 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -65,6 +65,9 @@ PODS: - libwebp/mux (1.2.4): - libwebp/demux - libwebp/webp (1.2.4) + - MMKV (1.2.14): + - MMKVCore (~> 1.2.14) + - MMKVCore (1.2.14) - RCT-Folly (2021.07.22.00): - boost - DoubleConversion @@ -305,6 +308,9 @@ PODS: - React - react-native-menu (0.7.2): - React + - react-native-mmkv (2.5.1): + - MMKV (>= 1.2.13) + - React-Core - react-native-netinfo (9.3.7): - React-Core - react-native-pager-view (6.1.2): @@ -494,6 +500,7 @@ DEPENDENCIES: - react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`) - react-native-language-detection (from `../node_modules/react-native-language-detection`) - "react-native-menu (from `../node_modules/@react-native-menu/menu`)" + - react-native-mmkv (from `../node_modules/react-native-mmkv`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) - "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)" @@ -527,6 +534,8 @@ SPEC REPOS: - fmt - libevent - libwebp + - MMKV + - MMKVCore - SDWebImage - SDWebImageWebPCoder - Sentry @@ -629,6 +638,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-language-detection" react-native-menu: :path: "../node_modules/@react-native-menu/menu" + react-native-mmkv: + :path: "../node_modules/react-native-mmkv" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" react-native-pager-view: @@ -714,6 +725,8 @@ SPEC CHECKSUMS: hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef + MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd + MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a RCTTypeSafety: 27c2ac1b00609a432ced1ae701247593f07f901e @@ -736,6 +749,7 @@ SPEC CHECKSUMS: react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0 react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24 + react-native-mmkv: 69b9c003f10afdd01addf7c6ee784ce42ee2eff3 react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c diff --git a/package.json b/package.json index ab857b27..612a5bbe 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@react-navigation/native": "^6.1.1", "@react-navigation/native-stack": "^6.9.7", "@react-navigation/stack": "^6.3.10", - "@reduxjs/toolkit": "^1.9.1", "@sentry/react-native": "4.12.0", "@sharcoux/slider": "^6.1.1", "@tanstack/react-query": "^4.20.4", @@ -79,6 +78,7 @@ "react-native-image-picker": "^4.10.3", "react-native-ios-context-menu": "^1.15.1", "react-native-language-detection": "^0.2.2", + "react-native-mmkv": "^2.5.1", "react-native-pager-view": "^6.1.2", "react-native-reanimated": "^2.13.0", "react-native-reanimated-zoom": "^0.3.3", @@ -89,7 +89,6 @@ "react-native-swipe-list-view": "^3.2.9", "react-native-tab-view": "^3.3.4", "react-redux": "^8.0.5", - "redux-persist": "^6.0.0", "rn-placeholder": "^3.0.3", "rtl-detect": "^1.0.4", "valid-url": "^1.0.9", diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 80b3e477..d2a83751 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -264,14 +264,6 @@ declare namespace Mastodon { } type Filter = T extends 'v2' ? Filter_V2 : Filter_V1 - type Filter_V1 = { - id: string - phrase: string - context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[] - expires_at?: string - irreversible: boolean - whole_word: boolean - } type Filter_V2 = { id: string title: string @@ -281,6 +273,14 @@ declare namespace Mastodon { keywords: FilterKeyword[] statuses: FilterStatus[] } + type Filter_V1 = { + id: string + phrase: string + context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[] + expires_at?: string + irreversible: boolean + whole_word: boolean + } type FilterKeyword = { id: string; keyword: string; whole_word: boolean } @@ -298,7 +298,45 @@ declare namespace Mastodon { replies_policy: 'none' | 'list' | 'followed' } - type Instance = { + type Instance = T extends 'v2' ? Instance_V2 : Instance_V1 + type Instance_V2 = { + domain: string + title: string + version: string + source_url: string + description: string + usage: { users: { active_month: number } } + thumbnail: { url: string; blurhash?: string; versions?: { '@1x'?: string; '@2x'?: string } } + languages: string[] + configuration: { + urls: { streaming_api: string } + accounts: { max_featured_tags: number } + statuses: { + max_characters: number + max_media_attachments: number + characters_reserved_per_url: number + } + media_attachments: { + supported_mime_types: string[] + image_size_limit: number + image_matrix_limit: number + video_size_limit: number + video_frame_rate_limit: number + video_matrix_limit: number + } + polls: { + max_options: number + max_characters_per_option: number + min_expiration: number + max_expiration: number + } + translation: { enabled: boolean } + registrations: { enabled: boolean; approval_required: boolean; message?: string } + contact: { email: string; account: Account } + rules: Rule[] + } + } + type Instance_V1 = { // Base uri: string title: string diff --git a/src/App.tsx b/src/App.tsx index a3f00f75..ed0cb520 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,30 +1,37 @@ import { ActionSheetProvider } from '@expo/react-native-action-sheet' -import getLanguage from '@helpers/getLanguage' -import queryClient from '@helpers/queryClient' -import i18n from '@root/i18n/i18n' -import Screens from '@root/Screens' -import audio from '@root/startup/audio' -import log from '@root/startup/log' -import netInfo from '@root/startup/netInfo' -import push from '@root/startup/push' -import sentry from '@root/startup/sentry' -import timezone from '@root/startup/timezone' -import { persistor, store } from '@root/store' import * as Sentry from '@sentry/react-native' +import { QueryClientProvider } from '@tanstack/react-query' import AccessibilityManager from '@utils/accessibility/AccessibilityManager' -import { changeLanguage } from '@utils/slices/settingsSlice' +import getLanguage from '@utils/helpers/getLanguage' +import queryClient from '@utils/queryHooks' +import audio from '@utils/startup/audio' +import log from '@utils/startup/log' +import netInfo from '@utils/startup/netInfo' +import push from '@utils/startup/push' +import sentry from '@utils/startup/sentry' +import timezone from '@utils/startup/timezone' +import { storage } from '@utils/storage' +import { + getGlobalStorage, + removeAccount, + setAccount, + setGlobalStorage +} from '@utils/storage/actions' +import { + hasMigratedFromAsyncStorage, + migrateFromAsyncStorage +} from '@utils/storage/migrations/toMMKV' import ThemeManager from '@utils/styles/ThemeManager' import * as Localization from 'expo-localization' import * as SplashScreen from 'expo-splash-screen' import React, { useCallback, useEffect, useState } from 'react' -import { IntlProvider } from 'react-intl' import { LogBox, Platform } from 'react-native' import { GestureHandlerRootView } from 'react-native-gesture-handler' +import { MMKV } from 'react-native-mmkv' import { SafeAreaProvider } from 'react-native-safe-area-context' import { enableFreeze } from 'react-native-screens' -import { QueryClientProvider } from '@tanstack/react-query' -import { Provider } from 'react-redux' -import { PersistGate } from 'redux-persist/integration/react' +import i18n from './i18n' +import Screens from './screens' Platform.select({ android: LogBox.ignoreLogs(['Setting a timer for a long period of time']) @@ -36,83 +43,97 @@ push() timezone() enableFreeze(true) +log('log', 'App', 'delay splash') +SplashScreen.preventAutoHideAsync() + const App: React.FC = () => { log('log', 'App', 'rendering App') + const [appIsReady, setAppIsReady] = useState(false) const [localCorrupt, setLocalCorrupt] = useState() + const [hasMigrated, setHasMigrated] = useState(hasMigratedFromAsyncStorage) + useEffect(() => { - const delaySplash = async () => { - log('log', 'App', 'delay splash') - try { - await SplashScreen.preventAutoHideAsync() - } catch (e) { - console.warn(e) + const prepare = async () => { + if (!hasMigrated && !hasMigratedFromAsyncStorage) { + try { + await migrateFromAsyncStorage() + setHasMigrated(true) + } catch (e) { + // TODO: fall back to AsyncStorage? Wipe storage clean and use MMKV? Crash app? + } + } else { + log('log', 'App', 'loading from MMKV') + const account = getGlobalStorage.string('account.active') + if (account) { + const storageAccount = new MMKV({ id: account }) + const token = storageAccount.getString('auth.token') + if (token) { + log('log', 'App', `Binding storage of ${account}`) + storage.account = storageAccount + } else { + log('log', 'App', `Token not found for ${account}`) + removeAccount(account) + } + } else { + log('log', 'App', 'No active account available') + const accounts = getGlobalStorage.object('accounts') + if (accounts?.length) { + log('log', 'App', `Setting active account ${accounts[accounts.length - 1]}`) + setAccount(accounts[accounts.length - 1]) + } else { + setGlobalStorage('account.active', undefined) + } + } } + + let netInfoRes = undefined + try { + netInfoRes = await netInfo() + } catch {} + + if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) { + setLocalCorrupt(netInfoRes.corrupted) + } + + log('log', 'App', `locale: ${Localization.locale}`) + const language = getLanguage() + if (!language) { + if (Platform.OS !== 'ios') { + setGlobalStorage('app.language', 'en') + } + i18n.changeLanguage('en') + } else { + i18n.changeLanguage(language) + } + + setAppIsReady(true) } - delaySplash() + + prepare() }, []) - - const onBeforeLift = useCallback(async () => { - let netInfoRes = undefined - try { - netInfoRes = await netInfo() - } catch {} - - if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) { - setLocalCorrupt(netInfoRes.corrupted) - } - - log('log', 'App', 'hide splash') - try { + const onLayoutRootView = useCallback(async () => { + if (appIsReady) { + log('log', 'App', 'hide splash') await SplashScreen.hideAsync() - return Promise.resolve() - } catch (e) { - console.warn(e) - return Promise.reject() } - }, []) + }, [appIsReady]) + if (!appIsReady) { + return null + } return ( - + - - { - log('log', 'App', 'bootstrapped') - if (bootstrapped) { - log('log', 'App', 'loading actual app :)') - log('log', 'App', `Locale: ${Localization.locale}`) - const language = getLanguage() - if (!language) { - if (Platform.OS !== 'ios') { - store.dispatch(changeLanguage('en')) - } - i18n.changeLanguage('en') - } else { - i18n.changeLanguage(language) - } - - return ( - - - - - - - - - - - - ) - } else { - return null - } - }} - /> - + + + + + + + + + ) diff --git a/src/components/AccountButton.tsx b/src/components/AccountButton.tsx index 21ef8288..d36f6bb4 100644 --- a/src/components/AccountButton.tsx +++ b/src/components/AccountButton.tsx @@ -1,19 +1,24 @@ import { useNavigation } from '@react-navigation/native' -import initQuery from '@utils/initQuery' -import { InstanceLatest } from '@utils/migrations/instances/migration' +import { generateAccountKey, getAccountDetails, setAccount } from '@utils/storage/actions' +import { StorageGlobal } from '@utils/storage/global' import { StyleConstants } from '@utils/styles/constants' import React from 'react' import Button from './Button' import haptics from './haptics' interface Props { - instance: InstanceLatest + account: NonNullable[number] selected?: boolean additionalActions?: () => void } -const AccountButton: React.FC = ({ instance, selected = false, additionalActions }) => { +const AccountButton: React.FC = ({ account, selected = false, additionalActions }) => { const navigation = useNavigation() + const accountDetails = getAccountDetails( + ['auth.account.acct', 'auth.domain', 'auth.account.id'], + account + ) + if (!accountDetails) return null return (