From c4fdf0ed6026e60a6b8a9ff7b62f67c7a62f4349 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Thu, 16 Jan 2025 00:37:29 +0100 Subject: [PATCH] feat(widgets): add calculator widget --- .tx/config | 6 + src/assets/icons/widgets.ts | 13 + src/components/BaseFeedWidget.tsx | 5 +- src/components/Widgets.tsx | 60 ++-- src/components/widgets/BaseWidget.tsx | 176 ++++++++++++ src/components/widgets/CalculatorWidget.tsx | 125 ++++++++ src/constants/widgets.ts | 14 + src/navigation/root/RootNavigator.tsx | 2 + src/navigation/types/index.ts | 13 +- src/screens/Wallets/MainOnboarding.tsx | 53 ++-- src/screens/Wallets/index.tsx | 8 +- src/screens/Widgets/FeedWidgetItems.tsx | 123 ++++++++ src/screens/Widgets/SlashfeedWidget.tsx | 302 ++++++++++++++++++++ src/screens/Widgets/Widget.tsx | 207 +++----------- src/screens/Widgets/WidgetEdit.tsx | 8 +- src/screens/Widgets/WidgetListItem.tsx | 81 ++++++ src/screens/Widgets/WidgetsSuggestions.tsx | 119 +------- src/store/index.ts | 2 +- src/store/migrations/index.ts | 16 ++ src/store/reselect/widgets.ts | 13 +- src/store/slices/backup.ts | 4 +- src/store/slices/widgets.ts | 19 +- src/store/types/widgets.ts | 7 +- src/styles/colors.ts | 2 + src/utils/i18n/locales/en/common.json | 6 + src/utils/i18n/locales/en/index.ts | 2 + src/utils/i18n/locales/en/slashtags.json | 3 - src/utils/i18n/locales/en/widgets.json | 58 ++++ src/utils/slashtags/index.ts | 2 +- 29 files changed, 1078 insertions(+), 371 deletions(-) create mode 100644 src/components/widgets/BaseWidget.tsx create mode 100644 src/components/widgets/CalculatorWidget.tsx create mode 100644 src/constants/widgets.ts create mode 100644 src/screens/Widgets/FeedWidgetItems.tsx create mode 100644 src/screens/Widgets/SlashfeedWidget.tsx create mode 100644 src/screens/Widgets/WidgetListItem.tsx create mode 100644 src/utils/i18n/locales/en/widgets.json diff --git a/.tx/config b/.tx/config index 70e31c9a5..17de42d28 100644 --- a/.tx/config +++ b/.tx/config @@ -60,3 +60,9 @@ file_filter = src/utils/i18n/locales//wallet.json source_file = src/utils/i18n/locales/en/wallet.json source_lang = en type = STRUCTURED_JSON + +[o:synonym:p:bitkit:r:widgets] +file_filter = src/utils/i18n/locales//widgets.json +source_file = src/utils/i18n/locales/en/widgets.json +source_lang = en +type = STRUCTURED_JSON diff --git a/src/assets/icons/widgets.ts b/src/assets/icons/widgets.ts index c715844fe..b3c97d7fb 100644 --- a/src/assets/icons/widgets.ts +++ b/src/assets/icons/widgets.ts @@ -1,2 +1,15 @@ export const bitfinexIcon = (): string => ''; + +export const calculatorIcon = (): string => + ` + + + + + + + + + +`; diff --git a/src/components/BaseFeedWidget.tsx b/src/components/BaseFeedWidget.tsx index 4507d310e..3dc3551f7 100644 --- a/src/components/BaseFeedWidget.tsx +++ b/src/components/BaseFeedWidget.tsx @@ -52,7 +52,7 @@ const BaseFeedWidget = ({ const widgetName = name ?? config?.name ?? url; const onEdit = (): void => { - rootNavigation.navigate('Widget', { url }); + rootNavigation.navigate('FeedWidget', { url }); }; const onDelete = (): void => { @@ -89,7 +89,6 @@ const BaseFeedWidget = ({ { const { t } = useTranslation('slashtags'); const dispatch = useAppDispatch(); + const widgets = useAppSelector(widgetsSelector); const sortOrder = useAppSelector(widgetsOrderSelector); const onboardedWidgets = useAppSelector(onboardedWidgetsSelector); @@ -45,15 +47,20 @@ const Widgets = (): ReactElement => { useFocusEffect(useCallback(() => setEditing(false), [])); const sortedWidgets = useMemo(() => { - const savedWidgets = Object.entries(widgets) as [string, TWidget][]; - return savedWidgets.sort( - ([a], [b]) => sortOrder.indexOf(a) - sortOrder.indexOf(b), - ); + const savedWidgets = Object.keys(widgets); + + const sorted = savedWidgets.sort((a, b) => { + const indexA = sortOrder.indexOf(a); + const indexB = sortOrder.indexOf(b); + return indexA - indexB; + }); + + return sorted; }, [widgets, sortOrder]); const onDragEnd = useCallback( ({ data }) => { - const order = data.map((i): string => i[0]); + const order = data.map((id): string => id); dispatch(setWidgetsSortOrder(order)); }, [dispatch], @@ -67,25 +74,41 @@ const Widgets = (): ReactElement => { }; const renderItem = useCallback( - ({ item, drag }: RenderItemParams<[string, TWidget]>): ReactElement => { - const [url, widget] = item; - - const _drag = (): void => { + ({ item: id, drag }: RenderItemParams): ReactElement => { + const initiateDrag = (): void => { // only allow dragging if there are more than 1 widget if (sortedWidgets.length > 1 && editing) { drag(); } }; - const feedWidget = widget as TFeedWidget; - let testID: string; let Component: | typeof PriceWidget | typeof HeadlinesWidget | typeof BlocksWidget | typeof FactsWidget - | typeof FeedWidget; + | typeof FeedWidget + | typeof CalculatorWidget; + + if (id === 'calculator') { + Component = CalculatorWidget; + testID = 'CalculatorWidget'; + + return ( + + + + ); + } + + const feedWidget = widgets[id] as TFeedWidget; switch (feedWidget.type) { case SUPPORTED_FEED_TYPES.PRICE_FEED: @@ -117,17 +140,17 @@ const Widgets = (): ReactElement => { ); }, - [editing, sortedWidgets.length], + [editing, widgets, sortedWidgets.length], ); return ( @@ -136,7 +159,6 @@ const Widgets = (): ReactElement => { {t('widgets')} {sortedWidgets.length > 0 && ( setEditing(!editing)}> @@ -151,7 +173,7 @@ const Widgets = (): ReactElement => { item[0]} + keyExtractor={(id): string => id} renderItem={renderItem} scrollEnabled={false} activationDistance={editing ? 0 : 100} diff --git a/src/components/widgets/BaseWidget.tsx b/src/components/widgets/BaseWidget.tsx new file mode 100644 index 000000000..88ea0e630 --- /dev/null +++ b/src/components/widgets/BaseWidget.tsx @@ -0,0 +1,176 @@ +import React, { memo, ReactElement, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; + +import { useNavigation } from '@react-navigation/native'; +import { widgets } from '../../constants/widgets'; +import { useAppDispatch, useAppSelector } from '../../hooks/redux'; +import { RootNavigationProp } from '../../navigation/types'; +import { showWidgetTitlesSelector } from '../../store/reselect/settings'; +import { deleteWidget } from '../../store/slices/widgets'; +import { TouchableOpacity } from '../../styles/components'; +import { ListIcon, SettingsIcon, TrashIcon } from '../../styles/icons'; +import { BodyMSB } from '../../styles/text'; +import { truncate } from '../../utils/helpers'; +import Dialog from '../Dialog'; +import SvgImage from '../SvgImage'; + +const BaseWidget = ({ + id, + children, + isEditing, + style, + testID, + onPress, + onPressIn, + onLongPress, +}: { + id: string; + children: ReactElement; + isEditing?: boolean; + style?: StyleProp; + testID?: string; + onPress?: () => void; + onPressIn?: () => void; + onLongPress?: () => void; +}): ReactElement => { + const { t } = useTranslation('widgets'); + const navigation = useNavigation(); + const dispatch = useAppDispatch(); + const [showDialog, setShowDialog] = useState(false); + const showTitle = useAppSelector(showWidgetTitlesSelector); + + const widget = { + name: t(`${id}.name`), + icon: widgets[id].icon, + }; + + const onEdit = (): void => { + navigation.navigate('Widget', { id }); + }; + + const onDelete = (): void => { + setShowDialog(true); + }; + + return ( + <> + + {(showTitle || isEditing) && ( + + + + + + + + {truncate(widget.name, 18)} + + + + {isEditing && ( + + + + + + + + + + + + )} + + )} + + {showTitle && !isEditing && } + + {!isEditing && children} + + + { + setShowDialog(false); + }} + onConfirm={(): void => { + dispatch(deleteWidget(id)); + setShowDialog(false); + }} + /> + + ); +}; + +const styles = StyleSheet.create({ + root: { + borderRadius: 16, + padding: 16, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + title: { + flexDirection: 'row', + alignItems: 'center', + }, + icon: { + marginRight: 16, + borderRadius: 6.4, + overflow: 'hidden', + height: 32, + width: 32, + }, + name: { + lineHeight: 22, + }, + actions: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + actionButton: { + alignItems: 'center', + justifyContent: 'center', + width: 32, + height: 32, + marginLeft: 8, + }, + spacer: { + height: 16, + }, + content: { + justifyContent: 'center', + }, +}); + +export default memo(BaseWidget); diff --git a/src/components/widgets/CalculatorWidget.tsx b/src/components/widgets/CalculatorWidget.tsx new file mode 100644 index 000000000..79e445852 --- /dev/null +++ b/src/components/widgets/CalculatorWidget.tsx @@ -0,0 +1,125 @@ +import React, { ReactElement, useState } from 'react'; +import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; + +import { useCurrency } from '../../hooks/displayValues'; +import { TextInput, View as ThemedView } from '../../styles/components'; +import { UnitBitcoinIcon } from '../../styles/icons'; +import { BodyMSB, CaptionB } from '../../styles/text'; +import { fiatToBitcoinUnit } from '../../utils/conversion'; +import { getDisplayValues } from '../../utils/displayValues'; +import BaseWidget from './BaseWidget'; + +const CalculatorWidget = ({ + isEditing = false, + style, + testID, + onPressIn, + onLongPress, +}: { + isEditing?: boolean; + style?: StyleProp; + testID?: string; + onPressIn?: () => void; + onLongPress?: () => void; +}): ReactElement => { + const { fiatSymbol, fiatTicker } = useCurrency(); + const [bitcoinAmount, setBitcoinAmount] = useState('10000'); + const [fiatAmount, setFiatAmount] = useState(() => { + const amount = Number(bitcoinAmount); + const dv = getDisplayValues({ satoshis: amount, shouldRoundUpFiat: true }); + return dv.fiatValue.toString(); + }); + + const updateFiatAmount = (bitcoin: string) => { + const amount = Number(bitcoin); + const dv = getDisplayValues({ satoshis: amount, shouldRoundUpFiat: true }); + setFiatAmount(dv.fiatValue.toString()); + }; + + const updateBitcoinAmount = (fiat: string) => { + const amount = Number(fiat.replace(',', '.')); + const sats = fiatToBitcoinUnit({ amount }); + setBitcoinAmount(sats.toString()); + }; + + return ( + + + + + + { + setBitcoinAmount(text); + updateFiatAmount(text); + }} + /> + + bitcoin + + + + {fiatSymbol} + + + { + setFiatAmount(text); + updateBitcoinAmount(text); + }} + /> + + {fiatTicker} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + gap: 16, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + borderRadius: 8, + height: 64, + paddingHorizontal: 16, + }, + icon: { + borderRadius: 32, + alignItems: 'center', + justifyContent: 'center', + height: 32, + width: 32, + }, + amount: { + flex: 1, + marginLeft: 8, + }, + input: { + flex: 1, + backgroundColor: 'transparent', + marginLeft: -16, + }, +}); + +export default CalculatorWidget; diff --git a/src/constants/widgets.ts b/src/constants/widgets.ts new file mode 100644 index 000000000..7cdd3c446 --- /dev/null +++ b/src/constants/widgets.ts @@ -0,0 +1,14 @@ +import { calculatorIcon } from '../assets/icons/widgets'; + +export const priceFeedUrl = + 'slashfeed:9ckhj7ea31ugskdewy9eiod5trhtbgcu9juza8aypjyugsp5f4oo/Bitcoin Price'; +export const newsFeedUrl = + 'slashfeed:tdm8gwcuh6nfw4w8remadpzid9cff4tpoyazeokzbrfnjrxyf9ho/Bitcoin Headlines'; +export const blocksFeedUrl = + 'slashfeed:xzdjbqu5ym3hk4xp993hg4ahzs1g4o85dhw9nnp4qcbd57pxndoo/Bitcoin Blocks'; +export const bitcoinFactsUrl = + 'slashfeed:jm1n46tbdbnz4soq8hqppbpwt3pqp3cp9qobdjgyk8mjmnmdj14o/Bitcoin Facts'; + +export const widgets = { + calculator: { id: 'calculator', icon: calculatorIcon() }, +}; diff --git a/src/navigation/root/RootNavigator.tsx b/src/navigation/root/RootNavigator.tsx index cdb491dfa..8226f847c 100644 --- a/src/navigation/root/RootNavigator.tsx +++ b/src/navigation/root/RootNavigator.tsx @@ -33,6 +33,7 @@ import Profile from '../../screens/Profile/Profile'; import ProfileEdit from '../../screens/Profile/ProfileEdit'; import ScannerScreen from '../../screens/Scanner/MainScanner'; import ForgotPIN from '../../screens/Settings/PIN/ForgotPIN'; +import FeedWidget from '../../screens/Widgets/SlashfeedWidget'; import Widget from '../../screens/Widgets/Widget'; import WidgetEdit from '../../screens/Widgets/WidgetEdit'; import WidgetsOnboarding from '../../screens/Widgets/WidgetsOnboarding'; @@ -225,6 +226,7 @@ const RootNavigator = (): ReactElement => { component={WidgetsSuggestions} /> + diff --git a/src/navigation/types/index.ts b/src/navigation/types/index.ts index 213585bbe..7b49dc68d 100644 --- a/src/navigation/types/index.ts +++ b/src/navigation/types/index.ts @@ -9,7 +9,7 @@ import { import type { RecoveryStackParamList } from '../../screens/Recovery/RecoveryNavigator'; import type { IActivityItem } from '../../store/types/activity'; -import type { TWidgetSettings } from '../../store/types/widgets'; +import type { TFeedWidgetOptions } from '../../store/types/widgets'; import type { BackupStackParamList } from '../bottom-sheet/BackupNavigation'; import type { LNURLWithdrawStackParamList } from '../bottom-sheet/LNURLWithdrawNavigation'; import type { OrangeTicketStackParamList } from '../bottom-sheet/OrangeTicketNavigation'; @@ -45,14 +45,9 @@ export type RootStackParamList = { BuyBitcoin: undefined; WidgetsOnboarding: undefined; WidgetsSuggestions: undefined; - Widget: { - url: string; - preview?: TWidgetSettings; - }; - WidgetEdit: { - url: string; - initialFields: TWidgetSettings; - }; + Widget: { id: string }; + FeedWidget: { url: string; preview?: TFeedWidgetOptions }; + WidgetEdit: { url: string; initialFields: TFeedWidgetOptions }; }; // Root Stack Navigator diff --git a/src/screens/Wallets/MainOnboarding.tsx b/src/screens/Wallets/MainOnboarding.tsx index 890fb2041..8593ffb19 100644 --- a/src/screens/Wallets/MainOnboarding.tsx +++ b/src/screens/Wallets/MainOnboarding.tsx @@ -3,17 +3,17 @@ import { Trans, useTranslation } from 'react-i18next'; import { StyleProp, ViewStyle } from 'react-native'; import WalletOnboarding from '../../components/WalletOnboarding'; +import { + blocksFeedUrl, + newsFeedUrl, + priceFeedUrl, +} from '../../constants/widgets'; import { useAppDispatch } from '../../hooks/redux'; import { useSlashfeedConfig } from '../../hooks/widgets'; import { updateSettings } from '../../store/slices/settings'; -import { setFeedWidget } from '../../store/slices/widgets'; +import { saveWidget } from '../../store/slices/widgets'; import { Display } from '../../styles/text'; import { SUPPORTED_FEED_TYPES } from '../../utils/widgets'; -import { - BlocksFeedURL, - NewsFeedURL, - PriceFeedURL, -} from '../Widgets/WidgetsSuggestions'; const MainOnboarding = ({ style, @@ -22,29 +22,35 @@ const MainOnboarding = ({ }): ReactElement => { const dispatch = useAppDispatch(); const { t } = useTranslation('onboarding'); - const priceConfig = useSlashfeedConfig({ url: PriceFeedURL }); - const newsConfig = useSlashfeedConfig({ url: NewsFeedURL }); - const blocksConfig = useSlashfeedConfig({ url: BlocksFeedURL }); + const priceConfig = useSlashfeedConfig({ url: priceFeedUrl }); + const newsConfig = useSlashfeedConfig({ url: newsFeedUrl }); + const blocksConfig = useSlashfeedConfig({ url: blocksFeedUrl }); const onHideOnboarding = (): void => { // add default widgets if (priceConfig) { dispatch( - setFeedWidget({ - url: PriceFeedURL, - type: SUPPORTED_FEED_TYPES.PRICE_FEED, - fields: priceConfig.fields.filter((f) => f.name === 'BTC/USD'), - extras: { period: '1D', showSource: false }, + saveWidget({ + id: priceFeedUrl, + options: { + url: priceFeedUrl, + type: SUPPORTED_FEED_TYPES.PRICE_FEED, + fields: priceConfig.fields.filter((f) => f.name === 'BTC/USD'), + extras: { period: '1D', showSource: false }, + }, }), ); } if (newsConfig) { dispatch( - setFeedWidget({ - url: NewsFeedURL, - type: SUPPORTED_FEED_TYPES.HEADLINES_FEED, - fields: newsConfig.fields, + saveWidget({ + id: newsFeedUrl, + options: { + url: newsFeedUrl, + type: SUPPORTED_FEED_TYPES.HEADLINES_FEED, + fields: newsConfig.fields, + }, }), ); } @@ -55,10 +61,13 @@ const MainOnboarding = ({ }); dispatch( - setFeedWidget({ - url: BlocksFeedURL, - type: SUPPORTED_FEED_TYPES.BLOCKS_FEED, - fields, + saveWidget({ + id: blocksFeedUrl, + options: { + url: blocksFeedUrl, + type: SUPPORTED_FEED_TYPES.BLOCKS_FEED, + fields, + }, }), ); } diff --git a/src/screens/Wallets/index.tsx b/src/screens/Wallets/index.tsx index 5c7511643..27f9dacf1 100644 --- a/src/screens/Wallets/index.tsx +++ b/src/screens/Wallets/index.tsx @@ -26,7 +26,7 @@ import { ignoresHideBalanceToastSelector, scanAllAddressesTimestampSelector, } from '../../store/reselect/user'; -import { widgetsSelector } from '../../store/reselect/widgets'; +import { hasWidgetsSelector } from '../../store/reselect/widgets'; import { updateSettings } from '../../store/slices/settings'; import { ignoreHideBalanceToast, updateUser } from '../../store/slices/user'; import { View as ThemedView } from '../../styles/components'; @@ -57,8 +57,8 @@ const Wallets = ({ navigation, onFocus }: Props): ReactElement => { scanAllAddressesTimestampSelector, ); const hideOnboardingSetting = useAppSelector(hideOnboardingMessageSelector); + const hasWidgets = useAppSelector(hasWidgetsSelector); const showWidgets = useAppSelector(showWidgetsSelector); - const widgets = useAppSelector(widgetsSelector); const insets = useSafeAreaInsets(); const { t } = useTranslation('wallet'); @@ -103,9 +103,7 @@ const Wallets = ({ navigation, onFocus }: Props): ReactElement => { }; const hideOnboarding = - hideOnboardingSetting || - totalBalance > 0 || - Object.keys(widgets).length > 0; + hideOnboardingSetting || totalBalance > 0 || hasWidgets; return ( diff --git a/src/screens/Widgets/FeedWidgetItems.tsx b/src/screens/Widgets/FeedWidgetItems.tsx new file mode 100644 index 000000000..2f4762719 --- /dev/null +++ b/src/screens/Widgets/FeedWidgetItems.tsx @@ -0,0 +1,123 @@ +import React, { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; +import { StyleSheet } from 'react-native'; + +import Divider from '../../components/Divider'; +import SvgImage from '../../components/SvgImage'; +import { + bitcoinFactsUrl, + blocksFeedUrl, + newsFeedUrl, + priceFeedUrl, +} from '../../constants/widgets'; +import { useSlashfeed } from '../../hooks/widgets'; +import { TouchableOpacity, View } from '../../styles/components'; +import { ChevronRight, QuestionMarkIcon } from '../../styles/icons'; +import { BodyMSB, CaptionB } from '../../styles/text'; +import { handleSlashtagURL } from '../../utils/slashtags'; + +const FeedWidgetItem = ({ + url, + testID, +}: { + url: string; + testID?: string; +}): ReactElement => { + const { t } = useTranslation('slashtags'); + const { config, icon, loading, failed } = useSlashfeed({ url }); + + if (loading || !config) { + return ( + handleSlashtagURL(url)}> + + + + + + {url} + + {failed + ? t('widget_failed_description') + : t('widget_loading_description')} + + + + + + + ); + } + + return ( + handleSlashtagURL(url)}> + + + {icon ? ( + + ) : ( + + )} + + + {config.name} + + {config.description} + + + + + + + ); +}; + +const FeedWidgetItems = (): ReactElement => ( + <> + + + + + +); + +const styles = StyleSheet.create({ + feed: { + flexDirection: 'row', + alignItems: 'center', + }, + feedDisabled: { + opacity: 0.5, + }, + text: { + flex: 1, + paddingRight: 20, + }, + icon: { + height: 48, + width: 48, + marginRight: 16, + borderRadius: 8, + overflow: 'hidden', + }, + arrow: { + marginLeft: 'auto', + }, +}); + +export default FeedWidgetItems; diff --git a/src/screens/Widgets/SlashfeedWidget.tsx b/src/screens/Widgets/SlashfeedWidget.tsx new file mode 100644 index 000000000..cbe053d31 --- /dev/null +++ b/src/screens/Widgets/SlashfeedWidget.tsx @@ -0,0 +1,302 @@ +import isEqual from 'lodash/isEqual'; +import React, { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; +import { StyleSheet, View } from 'react-native'; + +import BlocksWidget from '../../components/BlocksWidget'; +import FactsWidget from '../../components/FactsWidget'; +import FeedWidget from '../../components/FeedWidget'; +import HeadlinesWidget from '../../components/HeadlinesWidget'; +import HourglassSpinner from '../../components/HourglassSpinner'; +import LuganoFeedWidget from '../../components/LuganoFeedWidget'; +import NavigationHeader from '../../components/NavigationHeader'; +import PriceWidget from '../../components/PriceWidget'; +import SafeAreaInset from '../../components/SafeAreaInset'; +import SlashtagURL from '../../components/SlashtagURL'; +import Spinner from '../../components/Spinner'; +import SvgImage from '../../components/SvgImage'; +import Button from '../../components/buttons/Button'; +import { useAppDispatch, useAppSelector } from '../../hooks/redux'; +import { useSlashfeed } from '../../hooks/widgets'; +import type { RootStackScreenProps } from '../../navigation/types'; +import { widgetSelector } from '../../store/reselect/widgets'; +import { deleteWidget, saveWidget } from '../../store/slices/widgets'; +import { TFeedWidget } from '../../store/types/widgets'; +import { + ScrollView, + View as ThemedView, + TouchableOpacity, +} from '../../styles/components'; +import { ChevronRight, QuestionMarkIcon } from '../../styles/icons'; +import { BodyM, Caption13Up, Headline } from '../../styles/text'; +import { SUPPORTED_FEED_TYPES } from '../../utils/widgets'; +import { getDefaultSettings } from './WidgetEdit'; + +const SlashfeedWidget = ({ + navigation, + route, +}: RootStackScreenProps<'FeedWidget'>): ReactElement => { + const { url, preview } = route.params; + const { t } = useTranslation('slashtags'); + const { config, icon, loading } = useSlashfeed({ url }); + const dispatch = useAppDispatch(); + const savedWidget = useAppSelector((state) => { + return widgetSelector(state, url); + }) as TFeedWidget; + + const defaultSettings = getDefaultSettings(config); + const savedSelectedFields = savedWidget?.fields.map((f) => f.name); + const savedExtras = savedWidget?.extras; + + const settings = { + fields: preview?.fields ?? savedSelectedFields ?? defaultSettings.fields, + extras: preview?.extras ?? savedExtras ?? defaultSettings.extras, + }; + + const hasEdited = !isEqual(settings, defaultSettings); + + const onEdit = (): void => { + navigation.navigate('WidgetEdit', { + url, + initialFields: settings, + }); + }; + + const onDelete = (): void => { + dispatch(deleteWidget(url)); + navigation.popToTop(); + }; + + const onSave = (): void => { + if (config) { + const options = { + url, + type: config.type, + extras: settings.extras, + fields: config.fields.filter((f) => { + return settings.fields.includes(f.name); + }), + }; + + dispatch(saveWidget({ id: url, options })); + } + + navigation.popToTop(); + }; + + return ( + + + + + {!config ? ( + + ) : ( + + + + {config.name} + + + + {icon ? ( + + ) : ( + + )} + + + + {config.description && ( + + {config.description} + + )} + + {(config.type === SUPPORTED_FEED_TYPES.PRICE_FEED || + config.type === SUPPORTED_FEED_TYPES.BLOCKS_FEED) && ( + + + {t('widget_edit')} + + + + {hasEdited + ? t('widget_edit_custom') + : t('widget_edit_default')} + + + + + )} + + + + {t('widget_preview')} + + + {((): ReactElement => { + const previewWidget = { + url, + type: config.type, + extras: settings.extras, + fields: config.fields.filter((f) => { + return settings.fields.includes(f.name); + }), + }; + + let testID: string; + let Component: + | typeof PriceWidget + | typeof HeadlinesWidget + | typeof BlocksWidget + | typeof FactsWidget + | typeof FeedWidget; + + switch (config.type) { + case SUPPORTED_FEED_TYPES.PRICE_FEED: + Component = PriceWidget; + testID = 'PriceWidget'; + break; + case SUPPORTED_FEED_TYPES.HEADLINES_FEED: + Component = HeadlinesWidget; + testID = 'HeadlinesWidget'; + break; + case SUPPORTED_FEED_TYPES.BLOCKS_FEED: + Component = BlocksWidget; + testID = 'BlocksWidget'; + break; + case SUPPORTED_FEED_TYPES.FACTS_FEED: + Component = FactsWidget; + testID = 'FactsWidget'; + break; + case SUPPORTED_FEED_TYPES.LUGANO_FEED: + Component = LuganoFeedWidget; + testID = 'LuganoWidget'; + break; + default: + Component = FeedWidget; + testID = 'FeedWidget'; + } + + return !loading ? ( + + ) : ( + + + + ); + })()} + + + {savedWidget && ( +