diff --git a/package-lock.json b/package-lock.json index cac3aaba..6cc1af47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1348,9 +1348,9 @@ } }, "@react-native-async-storage/async-storage": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.13.0.tgz", - "integrity": "sha512-c+pKuUe54sysxnqsfG17kaAcd9xJQyTNYDQhZhYf3Ej5khAQPPM85eN2nc1sj1qEnxDde4mcfi3slrOd/KtoSw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.13.1.tgz", + "integrity": "sha512-yNTjYah8LuCZDqD+kbPdgyfht1uW2uEf5OKlaAthcmKz9wOf68ccO7PFXyLXlFIEZDWkRcEk4Cu4MSsQYI9pBQ==", "requires": { "deep-assign": "^3.0.0" } @@ -1692,52 +1692,58 @@ "resolved": "https://registry.npmjs.org/@react-native-community/segmented-control/-/segmented-control-2.1.1.tgz", "integrity": "sha512-vSrg+DIqX0zGeOb1o6oFLoWFFW8l1UEX/f7/9dXXzWHChF3rIqEpNHC4ONmsLJqWePN4E/n+k+q29z+GbqrqsQ==" }, + "@react-native-community/viewpager": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@react-native-community/viewpager/-/viewpager-4.1.6.tgz", + "integrity": "sha512-zNPQlgrPSB4JHT7w+kB7tTNsbP8H+Tqg1m+AszSArHCwSK+4dl1mgUt7hDd6XX3TTXpH2CdDsRx2cmJTTAweXw==" + }, "@react-navigation/bottom-tabs": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-5.9.2.tgz", - "integrity": "sha512-nSaAXFNbRukhMGq4U3m1rqbFZEPT64/gPTwUwQ5YQom5Q0a6xZ0AnOmtFIXvDBbK5VJiokGdBLJ/TMDchqcujQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-5.10.0.tgz", + "integrity": "sha512-nsdGtbFx2jrNJx26EXI4s4Nm+KxirkGCEsxwJ/XQk/EFjVHYFiFe5s1VLbirM1wi32rbUjJSgq7aD+mtgR0rAw==", "requires": { - "color": "^3.1.2", - "react-native-iphone-x-helper": "^1.2.1" + "color": "^3.1.3", + "react-native-iphone-x-helper": "^1.3.0" } }, "@react-navigation/core": { - "version": "5.12.5", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-5.12.5.tgz", - "integrity": "sha512-+QdQDtC75K1sBfACwCUNFlrCOYf36GYGr9YURHugm3LDEHUwAj7HPJA8FFLw1Rmp5N/P5q9Uct2B/XxJhYzKqA==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-5.13.0.tgz", + "integrity": "sha512-h6Y8daT1Q1ARSS6swjES+lO/ydBHmopIWCcMNs3PPjGZOQizqgvzq+h2lU9w+tZApEDad/mXP0mwYXheR2D91w==", "requires": { - "@react-navigation/routers": "^5.4.12", + "@react-navigation/routers": "^5.5.0", "escape-string-regexp": "^4.0.0", - "nanoid": "^3.1.12", - "query-string": "^6.13.5", + "nanoid": "^3.1.15", + "query-string": "^6.13.6", "react-is": "^16.13.0", - "use-subscription": "^1.4.0" + "use-subscription": "^1.5.0" } }, "@react-navigation/native": { - "version": "5.7.6", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-5.7.6.tgz", - "integrity": "sha512-u+o9Ifs4//Ah6UczXiaAV+hiWPL0NyTbErj5WayeQUd6K5IIbA1UwumRVWs2Xp8q/4Q9h6FpPHUcKOyiTxhaqA==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-5.8.0.tgz", + "integrity": "sha512-oIckbpcv4GMyHub8oV+rbVT7CDbUDSaQ0xW1tvDuooHnIZOwuQitA+JDslPW7dQK4U8uJIkdl8upWn0hnotDkQ==", "requires": { - "@react-navigation/core": "^5.12.5", - "nanoid": "^3.1.12" + "@react-navigation/core": "^5.13.0", + "escape-string-regexp": "^4.0.0", + "nanoid": "^3.1.15" } }, "@react-navigation/routers": { - "version": "5.4.12", - "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-5.4.12.tgz", - "integrity": "sha512-IwMmxeb5e6LboljhakmhtrHBXLYFrFDr2c1GjAG538e4MjT4QGi/ZYckAxCh/NqKI0knnzqKppPl2NsOMv/NoQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-5.5.0.tgz", + "integrity": "sha512-IUT37OPKu0AHEbECBvhvx8IafkoMVxyY82ldR4BhLYdKYVwgjK111m8WpkezbCeaMBkLHpamQHAMFjLFY4et9w==", "requires": { - "nanoid": "^3.1.12" + "nanoid": "^3.1.15" } }, "@react-navigation/stack": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-5.9.3.tgz", - "integrity": "sha512-/CJ5Rsrc9bMI20dD8oC/QSZHARMFuUv8DeoiYE7R2N0M44cFupF1CrzaZBBC/S4Zi1ahZ0A+Hj/gAzAEQrNTvA==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-5.10.0.tgz", + "integrity": "sha512-1+OVWPHTXc0HT4iMedVHWmIi4RnDOCfxmEjmEZfPCV+7BuzAJKAMW0KxJBFj3uAJscS/FxlZ3Qnjo5Z4aSNXqA==", "requires": { - "color": "^3.1.2", - "react-native-iphone-x-helper": "^1.2.1" + "color": "^3.1.3", + "react-native-iphone-x-helper": "^1.3.0" } }, "@reduxjs/toolkit": { @@ -2325,6 +2331,23 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } + }, "buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", @@ -2402,9 +2425,9 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "caniuse-lite": { - "version": "1.0.30001150", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001150.tgz", - "integrity": "sha512-kiNKvihW0m36UhAFnl7bOAv0i1K1f6wpfVtTF5O5O82XzgtBnb05V0XeV3oZ968vfg2sRNChsHw8ASH2hDfoYQ==" + "version": "1.0.30001151", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001151.tgz", + "integrity": "sha512-Zh3sHqskX6mHNrqUerh+fkf0N72cMxrmflzje/JyVImfpknscMnkeJrlFGJcqTmaa0iszdYptGpWMJCRQDkBVw==" }, "capture-exit": { "version": "2.0.0", @@ -2918,9 +2941,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.583", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.583.tgz", - "integrity": "sha512-L9BwLwJohjZW9mQESI79HRzhicPk1DFgM+8hOCfGgGCFEcA3Otpv7QK6SGtYoZvfQfE3wKLh0Hd5ptqUFv3gvQ==" + "version": "1.3.584", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.584.tgz", + "integrity": "sha512-NB3DzrTzJFhWkUp+nl2KtUtoFzrfGXTir2S+BU4tXGyXH9vlluPuFpE3pTKeH7+PY460tHLjKzh6K2+TWwW+Ww==" }, "emoji-regex": { "version": "8.0.0", @@ -3090,6 +3113,11 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, "exec-sh": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", @@ -3220,6 +3248,15 @@ "url-parse": "^1.4.4" } }, + "expo-av": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/expo-av/-/expo-av-8.6.0.tgz", + "integrity": "sha512-ZZJtCLCDigeDMu2fyv76Kyifu2quhTn6ViYjM2ku/HvFSvUxlb5kK9imuLog3kR+LRxeIiVS9F29r3PCOqI+rQ==", + "requires": { + "lodash": "^4.17.15", + "nullthrows": "^1.1.0" + } + }, "expo-constants": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-9.2.0.tgz", @@ -3265,9 +3302,9 @@ "integrity": "sha512-zlWGua8vm7+af4otaSpJlzu0SYIr0aWbL0qICySCDUEKkqig6MqfuI69NYHC0w9ocWZuh2xyj6Rbfy01UqcVSg==" }, "expo-linking": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-1.0.4.tgz", - "integrity": "sha512-tKZvn3D2t/rJQQbDXZaPl3pEZvyO2coSO1WHtXeOCUaWFjrrHxjW0HAZ2H2iR0zALPq/lXo0Po83RsES3E0DAg==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-1.0.5.tgz", + "integrity": "sha512-LH26/ilFU0rCdsO1SJbNqoii3jTBqHdEfSloXhEb73aKdQT2BG6z5IjFIQXV2RiCmxNJbotdbfXyWSPqPoCEwg==", "requires": { "expo-constants": "~9.2.0", "qs": "^6.5.0", @@ -3284,6 +3321,11 @@ "resolved": "https://registry.npmjs.org/expo-permissions/-/expo-permissions-9.3.0.tgz", "integrity": "sha512-ylSJZVvEGJVFTKsFrUL2S6gCvFt+/o8TJ3xT4WaMjHe2/2Z7R8ng6NR47Kt54t7XBIV/SZ7DIY9uRiR7TPuNYA==" }, + "expo-secure-store": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-9.2.0.tgz", + "integrity": "sha512-CtoMeuw/BzmLZMxmw30YiAZY51bpuOsBQpt3CrvLqpT2Q4/M18Tc1H4qXzHER3GPfZeG2nEJQkEgHsHXrIhPXg==" + }, "expo-splash-screen": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.6.2.tgz", @@ -3706,9 +3748,9 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==" + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" }, "get-caller-file": { "version": "2.0.5", @@ -3823,6 +3865,36 @@ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" }, + "html-entities": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", + "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==" + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "htmlparser2-without-node-native": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/htmlparser2-without-node-native/-/htmlparser2-without-node-native-3.9.2.tgz", @@ -3862,6 +3934,11 @@ "safer-buffer": ">= 2.1.2 < 3.0.0" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "image-size": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz", @@ -5632,9 +5709,9 @@ "optional": true }, "nanoid": { - "version": "3.1.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.15.tgz", - "integrity": "sha512-n8rXUZ8UU3lV6+43atPrSizqzh25n1/f00Wx1sCiE7R1sSHytZLTTiQl8DjC4IDLOnEZDlgJhy0yO4VsIpMxow==" + "version": "3.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz", + "integrity": "sha512-+AK8MN0WHji40lj8AEuwLOvLSbWYApQpre/aFJZD71r43wVRLrOYS4FmJOPQYon1TqB462RzrrxlfA74XRES8w==" }, "nanomatch": { "version": "1.2.13", @@ -6624,6 +6701,17 @@ "fbjs": "^1.0.0" } }, + "react-native-render-html": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/react-native-render-html/-/react-native-render-html-4.2.4.tgz", + "integrity": "sha512-OiLItEzKgS7dzD9XI5bHhjcUEfpWdzH1FgexzjbBdICPfYjmmcefpcRmLZY1+HMfxJ7wL8iF1PzTF48LchGTBA==", + "requires": { + "buffer": "^4.5.1", + "events": "^1.1.0", + "html-entities": "^1.2.0", + "htmlparser2": "3.10.1" + } + }, "react-native-safe-area-context": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-3.1.4.tgz", @@ -6667,15 +6755,15 @@ } }, "react-redux": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.1.tgz", - "integrity": "sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz", + "integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==", "requires": { - "@babel/runtime": "^7.5.5", - "hoist-non-react-statics": "^3.3.0", + "@babel/runtime": "^7.12.1", + "hoist-non-react-statics": "^3.3.2", "loose-envify": "^1.4.0", "prop-types": "^15.7.2", - "react-is": "^16.9.0" + "react-is": "^16.13.1" }, "dependencies": { "hoist-non-react-statics": { diff --git a/package.json b/package.json index 66acbea5..58e7faee 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,16 @@ "react-native-htmlview": "^0.16.0", "react-native-image-zoom-viewer": "^3.0.1", "react-native-reanimated": "~1.13.0", + "react-native-render-html": "^4.2.4", "react-native-safe-area-context": "3.1.4", "react-native-screens": "~2.10.1", "react-native-web": "~0.13.7", "react-native-webview": "10.7.0", - "react-redux": "^7.2.1" + "react-redux": "^7.2.1", + "expo-av": "~8.6.0", + "expo-secure-store": "~9.2.0", + "expo-splash-screen": "~0.6.1", + "@react-native-community/viewpager": "4.1.6" }, "devDependencies": { "@babel/core": "~7.9.0", diff --git a/src/components/ParseContent.jsx b/src/components/ParseContent.jsx index 0e3f6dbc..e0ec9486 100644 --- a/src/components/ParseContent.jsx +++ b/src/components/ParseContent.jsx @@ -4,7 +4,7 @@ import { StyleSheet, Text } from 'react-native' import HTMLView from 'react-native-htmlview' import { useNavigation } from '@react-navigation/native' -import Emojis from 'src/components/TootTimeline/Emojis' +import Emojis from 'src/components/Toot/Emojis' function renderNode ({ node, index, navigation, mentions, showFullLink }) { if (node.name == 'a') { @@ -72,7 +72,8 @@ export default function ParseContent ({ emojis, emojiSize = 14, mentions, - showFullLink = false + showFullLink = false, + linesTruncated = 10 }) { const navigation = useNavigation() @@ -80,13 +81,16 @@ export default function ParseContent ({ renderNode({ node, index, navigation, mentions, showFullLink }) } TextComponent={({ children }) => ( )} + RootComponent={({ children }) => { + return {children} + }} /> ) } @@ -115,5 +119,6 @@ ParseContent.propTypes = { acct: PropTypes.string.isRequired }) ), - showFullLink: PropTypes.bool + showFullLink: PropTypes.bool, + linesTruncated: PropTypes.number } diff --git a/src/components/Toot.jsx b/src/components/Toot.jsx new file mode 100644 index 00000000..1e0d3a36 --- /dev/null +++ b/src/components/Toot.jsx @@ -0,0 +1,129 @@ +import React, { useMemo } from 'react' +import PropTypes from 'prop-types' +import { Dimensions, Pressable, StyleSheet, View } from 'react-native' +import { useNavigation } from '@react-navigation/native' + +import Reblog from './Toot/Reblog' +import Avatar from './Toot/Avatar' +import Header from './Toot/Header' +import Content from './Toot/Content' +import Poll from './Toot/Poll' +import Media from './Toot/Media' +import Card from './Toot/Card' +import Actions from './Toot/Actions' + +// Maybe break away notification types? https://docs.joinmastodon.org/entities/notification/ + +export default function Toot ({ item, notification }) { + const navigation = useNavigation() + + let actualContent + if (notification && item.status) { + actualContent = item.status + } else if (item.reblog) { + actualContent = item.reblog + } else { + actualContent = item + } + + const toot = useMemo(() => { + return ( + + {item.reblog && ( + + )} + + + +
+ {/* Can pass toot info to next page to speed up performance */} + + navigation.navigate('Toot', { toot: actualContent.id }) + } + > + {actualContent.content ? ( + + ) : ( + <> + )} + {actualContent.poll && } + {actualContent.media_attachments && ( + + )} + {actualContent.card && } + + + + + + ) + }) + + return toot +} + +const styles = StyleSheet.create({ + tootTimeline: { + flex: 1, + flexDirection: 'column', + padding: 12 + }, + toot: { + flex: 1, + flexDirection: 'row' + }, + details: { + flex: 1, + flexGrow: 1 + } +}) + +Toot.propTypes = { + item: PropTypes.shape({ + account: PropTypes.shape({ + avatar: PropTypes.string.isRequired, + display_name: PropTypes.string.isRequired, + acct: PropTypes.string.isRequired + }).isRequired, + created_at: PropTypes.string.isRequired, + application: PropTypes.exact({ + name: PropTypes.string.isRequired, + website: PropTypes.string + }), + content: PropTypes.string + }).isRequired, + notification: PropTypes.bool +} diff --git a/src/components/Toot/Actions.jsx b/src/components/Toot/Actions.jsx new file mode 100644 index 00000000..64f279b0 --- /dev/null +++ b/src/components/Toot/Actions.jsx @@ -0,0 +1,48 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Pressable, StyleSheet, Text, View } from 'react-native' +import { Feather } from '@expo/vector-icons' + +export default function Actions ({ + replies_count, + reblogs_count, + reblogged, + favourites_count, + favourited +}) { + return ( + + + + {replies_count} + + + + {reblogs_count} + + + + {favourites_count} + + + + + + ) +} + +const styles = StyleSheet.create({ + actions: { + flex: 1, + flexDirection: 'row' + }, + action: { + width: '25%', + flexDirection: 'row', + justifyContent: 'center' + } +}) + +// Actions.propTypes = { +// uri: PropTypes.string +// } diff --git a/src/components/TootTimeline/Avatar.jsx b/src/components/Toot/Avatar.jsx similarity index 100% rename from src/components/TootTimeline/Avatar.jsx rename to src/components/Toot/Avatar.jsx diff --git a/src/components/Toot/Card.jsx b/src/components/Toot/Card.jsx new file mode 100644 index 00000000..8d6505e9 --- /dev/null +++ b/src/components/Toot/Card.jsx @@ -0,0 +1,73 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Image, Pressable, StyleSheet, Text, View } from 'react-native' +import { useNavigation } from '@react-navigation/native' + +export default function Card ({ card }) { + const navigation = useNavigation() + return ( + card && ( + { + navigation.navigate('Webview', { + uri: card.url + }) + }} + > + {card.image && ( + + + + )} + + {card.title} + {card.description ? ( + {card.description} + ) : ( + <> + )} + {card.url} + + + ) + ) +} + +const styles = StyleSheet.create({ + card: { + flex: 1, + flexDirection: 'row', + height: 70, + marginTop: 12 + }, + left: { + width: 70 + }, + image: { + width: '100%', + height: '100%' + }, + right: { + flex: 1 + } +}) + +Card.propTypes = { + card: PropTypes.exact({ + url: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.string, + type: PropTypes.oneOf(['link', 'photo', 'video']), + author_name: PropTypes.string, + author_url: PropTypes.string, + provider_name: PropTypes.string, + provider_url: PropTypes.string, + html: PropTypes.string, + width: PropTypes.number, + height: PropTypes.number, + image: PropTypes.string, + embed_url: PropTypes.string, + blurhash: PropTypes.string + }).isRequired +} diff --git a/src/components/Toot/Content.jsx b/src/components/Toot/Content.jsx new file mode 100644 index 00000000..4c33b450 --- /dev/null +++ b/src/components/Toot/Content.jsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react' +import PropTypes from 'prop-types' +import { Text } from 'react-native' +import Collapsible from 'react-native-collapsible' + +import ParseContent from 'src/components/ParseContent' + +export default function Content ({ content, emojis, mentions, spoiler_text }) { + const [spoilerCollapsed, setSpoilerCollapsed] = useState(true) + + return ( + <> + {content && + (spoiler_text ? ( + <> + + {spoiler_text}{' '} + setSpoilerCollapsed(!spoilerCollapsed)}> + 点击展开 + + + + + + + ) : ( + + ))} + + ) +} + +Content.propTypes = { + content: ParseContent.propTypes.content, + emojis: ParseContent.propTypes.emojis, + mentions: ParseContent.propTypes.mentions, + spoiler_text: PropTypes.string +} diff --git a/src/components/TootTimeline/Emojis.jsx b/src/components/Toot/Emojis.jsx similarity index 86% rename from src/components/TootTimeline/Emojis.jsx rename to src/components/Toot/Emojis.jsx index cf0e6d30..12a20e95 100644 --- a/src/components/TootTimeline/Emojis.jsx +++ b/src/components/Toot/Emojis.jsx @@ -2,7 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { Image, Text } from 'react-native' -const regexEmoji = new RegExp(/(:.*?:)/g) +const regexEmoji = new RegExp(/(:[a-z0-9_]+:)/) export default function Emojis ({ content, emojis, dimension }) { const hasEmojis = content.match(regexEmoji) @@ -13,7 +13,11 @@ export default function Emojis ({ content, emojis, dimension }) { const emojiIndex = emojis.findIndex(emoji => { return emojiShortcode === `:${emoji.shortcode}:` }) - return ( + return emojiIndex === -1 ? ( + + Something wrong with emoji! + + ) : ( - @{account} + + @{account} + diff --git a/src/components/TootTimeline/Content.jsx b/src/components/Toot/Media.jsx similarity index 61% rename from src/components/TootTimeline/Content.jsx rename to src/components/Toot/Media.jsx index 709a360e..dfb60d56 100644 --- a/src/components/TootTimeline/Content.jsx +++ b/src/components/Toot/Media.jsx @@ -9,12 +9,9 @@ import { Pressable, View } from 'react-native' -import Collapsible from 'react-native-collapsible' import ImageViewer from 'react-native-image-zoom-viewer' -import ParseContent from 'src/components/ParseContent' - -function Media ({ media_attachments, sensitive, width }) { +export default function Media ({ media_attachments, sensitive, width }) { const [mediaSensitive, setMediaSensitive] = useState(sensitive) const [imageModalVisible, setImageModalVisible] = useState(false) const [imageModalIndex, setImageModalIndex] = useState(0) @@ -26,6 +23,7 @@ function Media ({ media_attachments, sensitive, width }) { } }, [mediaSensitive]) + let media let images = [] if (width) { media_attachments = media_attachments.map((m, i) => { @@ -41,7 +39,7 @@ function Media ({ media_attachments, sensitive, width }) { return ( { setImageModalIndex(i) setImageModalVisible(true) @@ -57,7 +55,7 @@ function Media ({ media_attachments, sensitive, width }) { } }) if (images) { - return ( + media = ( <> {media_attachments} @@ -95,69 +93,25 @@ function Media ({ media_attachments, sensitive, width }) { ) } else { - return {media_attachments} + media = {media_attachments} } } else { - return <> + media = <> } -} - -export default function Content ({ - content, - emojis, - media_attachments, - mentions, - sensitive, - spoiler_text, - width -}) { - const [spoilerCollapsed, setSpoilerCollapsed] = useState(true) return ( - <> - {content && - (spoiler_text ? ( - <> - - {spoiler_text}{' '} - setSpoilerCollapsed(!spoilerCollapsed)}> - 点击展开 - - - - - - - ) : ( - - ))} - {media_attachments.length > 0 && ( - - - - )} - + media_attachments.length > 0 && ( + + {media} + + ) ) } @@ -176,12 +130,8 @@ const styles = StyleSheet.create({ } }) -Content.propTypes = { - content: ParseContent.propTypes.content, - emojis: ParseContent.propTypes.emojis, +Media.propTypes = { // media_attachments - mentions: ParseContent.propTypes.mentions, sensitive: PropTypes.bool.isRequired, - spoiler_text: PropTypes.string, width: PropTypes.number.isRequired } diff --git a/src/components/Toot/Poll.jsx b/src/components/Toot/Poll.jsx new file mode 100644 index 00000000..d426d3a4 --- /dev/null +++ b/src/components/Toot/Poll.jsx @@ -0,0 +1,63 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { StyleSheet, Text, View } from 'react-native' + +import Emojis from './Emojis' + +export default function Poll ({ poll }) { + return ( + + {poll.options.map((option, index) => ( + + + + {Math.round((option.votes_count / poll.votes_count) * 100)}% + + {option.title} + + + + ))} + + ) +} + +const styles = StyleSheet.create({ + avatar: { + width: 50, + height: 50, + marginRight: 8 + }, + image: { + width: '100%', + height: '100%' + } +}) + +Poll.propTypes = { + poll: PropTypes.exact({ + id: PropTypes.string.isRequired, + expires_at: PropTypes.string.isRequired, + expired: PropTypes.bool.isRequired, + multiple: PropTypes.bool.isRequired, + votes_count: PropTypes.number, + voters_count: PropTypes.number, + voted: PropTypes.bool.isRequired, + own_votes: PropTypes.array, + options: PropTypes.arrayOf( + PropTypes.exact({ + title: PropTypes.string.isRequired, + votes_count: PropTypes.number.isRequired + }) + ), + emojis: Emojis.propTypes.emojis + }).isRequired +} diff --git a/src/components/TootTimeline/Reblog.jsx b/src/components/Toot/Reblog.jsx similarity index 100% rename from src/components/TootTimeline/Reblog.jsx rename to src/components/Toot/Reblog.jsx diff --git a/src/components/TootTimeline.jsx b/src/components/TootTimeline.jsx deleted file mode 100644 index 730ca875..00000000 --- a/src/components/TootTimeline.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from 'react' -import PropTypes from 'prop-types' -import { Dimensions, StyleSheet, View } from 'react-native' - -import Reblog from './TootTimeline/Reblog' -import Avatar from './TootTimeline/Avatar' -import Header from './TootTimeline/Header' -import Content from './TootTimeline/Content' -import Actions from './TootTimeline/Actions' - -// Maybe break away notification types? https://docs.joinmastodon.org/entities/notification/ - -export default function TootTimeline ({ item, notification }) { - let contentAggregated = {} - let actualContent - if (notification && item.status) { - actualContent = item.status - } else if (item.reblog) { - actualContent = item.reblog - } else { - actualContent = item - } - contentAggregated = { - content: actualContent.content, - emojis: actualContent.emojis, - media_attachments: actualContent.media_attachments, - mentions: actualContent.mentions, - sensitive: actualContent.sensitive, - spoiler_text: actualContent.spoiler_text, - tags: actualContent.tags - } - - return ( - - {item.reblog && ( - - )} - - - -
- - - - - - ) -} - -const styles = StyleSheet.create({ - tootTimeline: { - flex: 1, - flexDirection: 'column', - padding: 12 - }, - toot: { - flex: 1, - flexDirection: 'row' - }, - details: { - flex: 1, - flexGrow: 1 - } -}) - -TootTimeline.propTypes = { - item: PropTypes.shape({ - account: PropTypes.shape({ - avatar: PropTypes.string.isRequired, - display_name: PropTypes.string.isRequired, - acct: PropTypes.string.isRequired - }).isRequired, - created_at: PropTypes.string.isRequired, - application: PropTypes.exact({ - name: PropTypes.string.isRequired, - website: PropTypes.string - }), - content: PropTypes.string - }).isRequired, - notification: PropTypes.bool -} diff --git a/src/components/TootTimeline/Actions.jsx b/src/components/TootTimeline/Actions.jsx deleted file mode 100644 index 8d74b58c..00000000 --- a/src/components/TootTimeline/Actions.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { StyleSheet } from 'react-native' - -export default function Actions () { - return <> -} - -const styles = StyleSheet.create({ - width: 50, - height: 50 -}) - -// Actions.propTypes = { -// uri: PropTypes.string -// } diff --git a/src/stacks/Shared/Account.jsx b/src/stacks/Shared/Account.jsx index 57e37d2a..c9289ad6 100644 --- a/src/stacks/Shared/Account.jsx +++ b/src/stacks/Shared/Account.jsx @@ -1,21 +1,24 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Dimensions, + FlatList, Image, ScrollView, StyleSheet, Text, View } from 'react-native' -import HTMLView from 'react-native-htmlview' +import SegmentedControl from '@react-native-community/segmented-control' import { useDispatch, useSelector } from 'react-redux' import { useFocusEffect } from '@react-navigation/native' import { Feather } from '@expo/vector-icons' import * as accountSlice from 'src/stacks/common/accountSlice' import * as relationshipsSlice from 'src/stacks/common/relationshipsSlice' +import * as timelineSlice from 'src/stacks/common/timelineSlice' import ParseContent from 'src/components/ParseContent' +import Timeline from 'src/stacks/common/Timeline' // Moved account example: https://m.cmx.im/web/accounts/27812 @@ -65,10 +68,7 @@ function Information ({ account, emojis }) { ))} - - - - + {account.note && } 加入时间{' '} {new Date(account.created_at).toLocaleDateString('zh-CN', { @@ -85,13 +85,89 @@ function Information ({ account, emojis }) { ) } +function Toots ({ account }) { + const [segment, setSegment] = useState(0) + const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState( + false + ) + const horizontalPaging = useRef() + + const pages = ['Account_Default', 'Account_All', 'Account_Media'] + + return ( + <> + { + setSegmentManuallyTriggered(true) + setSegment(nativeEvent.selectedSegmentIndex) + horizontalPaging.current.scrollToIndex({ + index: nativeEvent.selectedSegmentIndex + }) + }} + style={{ width: '100%', height: 30 }} + /> + page} + renderItem={({ item, index }) => { + return ( + + + + ) + }} + ref={horizontalPaging} + bounces={false} + getItemLayout={(data, index) => ({ + length: Dimensions.get('window').width, + offset: Dimensions.get('window').width * index, + index + })} + horizontal + ListHeaderComponent={ + + Test + + } + onMomentumScrollEnd={() => { + setSegmentManuallyTriggered(false) + }} + onScroll={({ nativeEvent }) => + !segmentManuallyTriggered && + setSegment( + nativeEvent.contentOffset.x <= Dimensions.get('window').width / 3 + ? 0 + : 1 + ) + } + pagingEnabled + showsHorizontalScrollIndicator={false} + /> + + ) +} + export default function Account ({ route: { params: { id } } }) { const dispatch = useDispatch() - const accountState = useSelector(accountSlice.retrive) + const accountState = useSelector(state => state.account) // const stateRelationships = useSelector(relationshipsState) const [loaded, setLoaded] = useState(false) const [headerImageSize, setHeaderImageSize] = useState() @@ -100,7 +176,6 @@ export default function Account ({ if (accountState.status === 'idle') { dispatch(accountSlice.fetch({ id })) } - if (accountState.account.header) { Image.getSize(accountState.account.header, (width, height) => { setHeaderImageSize({ width, height }) @@ -117,12 +192,15 @@ export default function Account ({ return () => { dispatch(accountSlice.reset()) + dispatch(timelineSlice.reset('Account_Default')) + dispatch(timelineSlice.reset('Account_All')) + dispatch(timelineSlice.reset('Account_Media')) } }, []) ) // add emoji support return loaded ? ( - +
- + {accountState.account.id && } + ) : ( <> ) diff --git a/src/stacks/Shared/Toot.jsx b/src/stacks/Shared/Toot.jsx new file mode 100644 index 00000000..9b88515f --- /dev/null +++ b/src/stacks/Shared/Toot.jsx @@ -0,0 +1,28 @@ +import React from 'react' +import { useDispatch } from 'react-redux' +import { useFocusEffect } from '@react-navigation/native' + +import Timeline from 'src/stacks/common/Timeline' +import { reset } from 'src/stacks/common/timelineSlice' + +// Show remote hashtag? Only when private, show local version? + +export default function Toot ({ + route: { + params: { toot } + } +}) { + const dispatch = useDispatch() + + useFocusEffect( + React.useCallback(() => { + // Do something when the screen is focused + + return () => { + dispatch(reset('Toot')) + } + }, []) + ) + + return +} diff --git a/src/stacks/Shared/Webview.jsx b/src/stacks/Shared/Webview.jsx index 07b4e757..e3637ff1 100644 --- a/src/stacks/Shared/Webview.jsx +++ b/src/stacks/Shared/Webview.jsx @@ -2,7 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { WebView } from 'react-native-webview' -// Webview preview card +// Update page title export default function Webview ({ route: { diff --git a/src/stacks/common/Timeline.jsx b/src/stacks/common/Timeline.jsx index 0b65245e..075b0552 100644 --- a/src/stacks/common/Timeline.jsx +++ b/src/stacks/common/Timeline.jsx @@ -1,63 +1,73 @@ import React, { useEffect, useState } from 'react' import PropTypes from 'prop-types' -import { ActivityIndicator, FlatList, View } from 'react-native' +import { ActivityIndicator, FlatList, Text, View } from 'react-native' import { useSelector, useDispatch } from 'react-redux' -import TootTimeline from 'src/components/TootTimeline' -import { fetch, getState } from './timelineSlice' +import Toot from 'src/components/Toot' +import { fetch } from './timelineSlice' // Opening nesting hashtag pages -export default function Timeline ({ page, hashtag, list }) { +export default function Timeline ({ + page, + hashtag, + list, + toot, + account, + disableRefresh +}) { const dispatch = useDispatch() - const state = useSelector(state => getState(state)(page)) + const state = useSelector(state => state.timelines[page]) const [timelineReady, setTimelineReady] = useState(false) useEffect(() => { if (state.status === 'idle') { - dispatch(fetch({ page, hashtag, list })) + dispatch(fetch({ page, hashtag, list, toot, account })) setTimelineReady(true) } }, [state, dispatch]) let content - if (state.status === 'error') { + if (state.status === 'failed') { content = Error message } else { content = ( <> id} renderItem={({ item, index, separators }) => ( - + )} - onRefresh={() => - dispatch( - fetch({ - page, - query: [{ key: 'since_id', value: state.toots[0].id }] - }) - ) - } - refreshing={state.status === 'loading'} - onEndReached={() => { - if (!timelineReady) { + {...(state.pointer && { initialScrollIndex: state.pointer })} + {...(!disableRefresh && { + onRefresh: () => dispatch( fetch({ page, - query: [ - { - key: 'max_id', - value: state.toots[state.toots.length - 1].id - } - ] + query: [{ key: 'since_id', value: state.toots[0].id }] }) - ) - setTimelineReady(true) - } - }} - onEndReachedThreshold={0.5} + ), + refreshing: state.status === 'loading', + onEndReached: () => { + if (!timelineReady) { + dispatch( + fetch({ + page, + query: [ + { + key: 'max_id', + value: state.toots[state.toots.length - 1].id + } + ] + }) + ) + setTimelineReady(true) + } + }, + onEndReachedThreshold: 0.5 + })} onMomentumScrollBegin={() => setTimelineReady(false)} /> {state.status === 'loading' && } @@ -71,5 +81,7 @@ export default function Timeline ({ page, hashtag, list }) { Timeline.propTypes = { page: PropTypes.string.isRequired, hashtag: PropTypes.string, - list: PropTypes.string + list: PropTypes.string, + toot: PropTypes.string, + disableRefresh: PropTypes.bool } diff --git a/src/stacks/common/TimelinesCombined.jsx b/src/stacks/common/TimelinesCombined.jsx index 3a6b5927..8eedae0a 100644 --- a/src/stacks/common/TimelinesCombined.jsx +++ b/src/stacks/common/TimelinesCombined.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react' import PropTypes from 'prop-types' -import { Animated, Dimensions, Text, View } from 'react-native' +import { Dimensions, FlatList, View } from 'react-native' import { createNativeStackNavigator } from 'react-native-screens/native-stack' import SegmentedControl from '@react-native-community/segmented-control' import { Feather } from '@expo/vector-icons' @@ -8,20 +8,32 @@ import { Feather } from '@expo/vector-icons' import Timeline from './Timeline' import Account from 'src/stacks/Shared/Account' import Hashtag from 'src/stacks/Shared/Hashtag' +import Toot from 'src/stacks/Shared/Toot' import Webview from 'src/stacks/Shared/Webview' const Stack = createNativeStackNavigator() +function Page ({ item: { page } }) { + return ( + + + + ) +} + export default function TimelinesCombined ({ name, content }) { const [segment, setSegment] = useState(0) const [renderHeader, setRenderHeader] = useState(false) + const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState( + false + ) useEffect(() => { const nbr = setTimeout(() => setRenderHeader(true), 50) return }, []) - const moveAnimation = useRef(new Animated.Value(0)).current + const horizontalPaging = useRef() return ( @@ -38,37 +50,47 @@ export default function TimelinesCombined ({ name, content }) { { - setSegment(e.nativeEvent.selectedSegmentIndex) - Animated.timing(moveAnimation, { - toValue: - -e.nativeEvent.selectedSegmentIndex * - Dimensions.get('window').width, - duration: 250, - useNativeDriver: false - }).start() + onChange={({ nativeEvent }) => { + setSegmentManuallyTriggered(true) + setSegment(nativeEvent.selectedSegmentIndex) + horizontalPaging.current.scrollToIndex({ + index: nativeEvent.selectedSegmentIndex + }) }} style={{ width: 150, height: 30 }} /> ) : null }} > - {props => ( - ( + page} + renderItem={({ item, index }) => { + return }} - {...props} - > - - - - - - - + ref={horizontalPaging} + bounces={false} + getItemLayout={(data, index) => ({ + length: Dimensions.get('window').width, + offset: Dimensions.get('window').width * index, + index + })} + horizontal + onMomentumScrollEnd={() => setSegmentManuallyTriggered(false)} + onScroll={({ nativeEvent }) => + !segmentManuallyTriggered && + setSegment( + nativeEvent.contentOffset.x <= + Dimensions.get('window').width / 2 + ? 0 + : 1 + ) + } + pagingEnabled + showsHorizontalScrollIndicator={false} + /> )} + ({ + title: '对话' + })} + /> ({ - title: `${route.params.domain}` - })} + // options={({ route }) => ({ + // title: `${route.params.domain}` + // })} /> ) diff --git a/src/stacks/common/accountSlice.js b/src/stacks/common/accountSlice.js index cd481bbc..3cee6ab5 100644 --- a/src/stacks/common/accountSlice.js +++ b/src/stacks/common/accountSlice.js @@ -16,8 +16,6 @@ const accountInitState = { status: 'idle' } -export const retrive = state => state.account - export const accountSlice = createSlice({ name: 'account', initialState: accountInitState, diff --git a/src/stacks/common/relationshipsSlice.js b/src/stacks/common/relationshipsSlice.js index fb599d3d..65351104 100644 --- a/src/stacks/common/relationshipsSlice.js +++ b/src/stacks/common/relationshipsSlice.js @@ -30,8 +30,6 @@ const relationshipsInitState = { status: 'idle' } -export const retrive = state => state.relationships - export const relationshipSlice = createSlice({ name: 'relationships', initialState: { diff --git a/src/stacks/common/timelineSlice.js b/src/stacks/common/timelineSlice.js index 7ea71f37..2b3d2c25 100644 --- a/src/stacks/common/timelineSlice.js +++ b/src/stacks/common/timelineSlice.js @@ -13,9 +13,10 @@ import { client } from 'src/api/client' export const fetch = createAsyncThunk( 'timeline/fetch', - async ({ page, query = [], hashtag, list }, { getState }) => { + async ({ page, query = [], account, hashtag, list, toot }, { getState }) => { const instanceLocal = getState().instanceInfo.local + '/api/v1/' const instanceRemote = getState().instanceInfo.remote + '/api/v1/' + // If header if needed for remote server const header = { headers: { Authorization: `Bearer ${getState().instanceInfo.localToken}` @@ -24,36 +25,103 @@ export const fetch = createAsyncThunk( switch (page) { case 'Following': - return await client.get(`${instanceLocal}timelines/home`, query, header) + return { + toots: await client.get( + `${instanceLocal}timelines/home`, + query, + header + ) + } case 'Local': query.push({ key: 'local', value: 'true' }) - return await client.get( - `${instanceLocal}timelines/public`, - query, - header - ) + return { + toots: await client.get( + `${instanceLocal}timelines/public`, + query, + header + ) + } case 'LocalPublic': - return await client.get( - `${instanceLocal}timelines/public`, - query, - header - ) + return { + toots: await client.get( + `${instanceLocal}timelines/public`, + query, + header + ) + } case 'RemotePublic': - return await client.get(`${instanceRemote}timelines/public`, query) + return { + toots: await client.get(`${instanceRemote}timelines/public`, query) + } case 'Notifications': - return await client.get(`${instanceLocal}notifications`, query, header) + return { + toots: await client.get( + `${instanceLocal}notifications`, + query, + header + ) + } + case 'Account_Default': + const toots = await client.get( + `${instanceLocal}accounts/${account}/statuses`, + [{ key: 'pinned', value: 'true' }], + header + ) + toots.push( + ...(await client.get( + `${instanceLocal}accounts/${account}/statuses`, + [{ key: 'exclude_replies', value: 'true' }], + header + )) + ) + return { toots: toots } + case 'Account_All': + return { + toots: await client.get( + `${instanceLocal}accounts/${account}/statuses`, + query, + header + ) + } + case 'Account_Media': + return { + toots: await client.get( + `${instanceLocal}accounts/${account}/statuses`, + [{ key: 'only_media', value: 'true' }], + header + ) + } case 'Hashtag': - return await client.get( - `${instanceLocal}timelines/tag/${hashtag}`, - query, - header - ) + return { + toots: await client.get( + `${instanceLocal}timelines/tag/${hashtag}`, + query, + header + ) + } case 'List': - return await client.get( - `${instanceLocal}timelines/list/${list}`, - query, + return { + toots: await client.get( + `${instanceLocal}timelines/list/${list}`, + query, + header + ) + } + case 'Toot': + const current = await client.get( + `${instanceLocal}statuses/${toot}`, + [], header ) + const context = await client.get( + `${instanceLocal}statuses/${toot}/context`, + [], + header + ) + return { + toots: [...context.ancestors, current, ...context.descendants], + pointer: context.ancestors.length + } default: console.error('Timeline type error') } @@ -62,11 +130,10 @@ export const fetch = createAsyncThunk( const timelineInitState = { toots: [], + pointer: undefined, status: 'idle' } -export const getState = state => page => state.timelines[page] - export const timelineSlice = createSlice({ name: 'timeline', initialState: { @@ -76,7 +143,11 @@ export const timelineSlice = createSlice({ RemotePublic: timelineInitState, Notifications: timelineInitState, Hashtag: timelineInitState, - List: timelineInitState + List: timelineInitState, + Toot: timelineInitState, + Account_Default: timelineInitState, + Account_All: timelineInitState, + Account_Media: timelineInitState }, reducers: { reset (state, action) { @@ -89,11 +160,15 @@ export const timelineSlice = createSlice({ }, [fetch.fulfilled]: (state, action) => { state[action.meta.arg.page].status = 'succeeded' - if (action.meta.arg.query && action.meta.arg.query.since_id) { - state[action.meta.arg.page].toots.unshift(...action.payload) + if ( + action.meta.arg.query && + action.meta.arg.query[0].key === 'since_id' + ) { + state[action.meta.arg.page].toots.unshift(...action.payload.toots) } else { - state[action.meta.arg.page].toots.push(...action.payload) + state[action.meta.arg.page].toots.push(...action.payload.toots) } + state[action.meta.arg.page].pointer = action.payload.pointer }, [fetch.rejected]: (state, action) => { state[action.meta.arg.page].status = 'failed'