Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: calculator widget #2436

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .tx/config
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,9 @@ file_filter = src/utils/i18n/locales/<lang>/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/<lang>/widgets.json
source_file = src/utils/i18n/locales/en/widgets.json
source_lang = en
type = STRUCTURED_JSON
13 changes: 13 additions & 0 deletions src/assets/icons/widgets.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
export const bitfinexIcon = (): string =>
'<svg fill="none" height="40" viewBox="0 0 40 40" width="40" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m10 10h20v20h-20z"/></clipPath><path d="m0 0h40v40h-40z" fill="#0e3452"/><g clip-path="url(#a)" fill="#03ca9b"><path d="m10.1704 22.995c-.17231-2.788.9987-5.887 3.4105-8.2988 5.2443-5.24423 16.1656-4.5092 16.2278-4.5049-.0299.0432-8.0138 11.619-17.4749 12.7017-.7314.0837-1.4542.1166-2.1634.102z"/><path d="m11.2784 26.5596c.2701.4307.5894.8341.9588 1.2034 3.2372 3.2372 9.0874 2.6356 13.0668-1.3438 5.2591-5.2592 4.5047-16.2279 4.5047-16.2279-.0291.065-5.7521 12.8938-14.8479 15.692-1.2347.3799-2.4724.6012-3.6824.6763z"/></g></svg>';

export const calculatorIcon = (): string =>
`<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="8" fill="#FF4400"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 40.75C17 40.0596 17.5596 39.5 18.25 39.5H28.25C28.9404 39.5 29.5 40.0596 29.5 40.75C29.5 41.4404 28.9404 42 28.25 42H18.25C17.5596 42 17 41.4404 17 40.75Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.25 34.5C23.9404 34.5 24.5 35.0596 24.5 35.75V45.75C24.5 46.4404 23.9404 47 23.25 47C22.5596 47 22 46.4404 22 45.75V35.75C22 35.0596 22.5596 34.5 23.25 34.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 23.25C17 22.5596 17.5596 22 18.25 22H28.25C28.9404 22 29.5 22.5596 29.5 23.25C29.5 23.9404 28.9404 24.5 28.25 24.5H18.25C17.5596 24.5 17 23.9404 17 23.25Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.5 38.2607C34.5 37.5704 35.0596 37.0107 35.75 37.0107H45.75C46.4404 37.0107 47 37.5704 47 38.2607C47 38.9511 46.4404 39.5107 45.75 39.5107H35.75C35.0596 39.5107 34.5 38.9511 34.5 38.2607Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.5 43.2393C34.5 42.5489 35.0596 41.9893 35.75 41.9893H45.75C46.4404 41.9893 47 42.5489 47 43.2393C47 43.9296 46.4404 44.4893 45.75 44.4893H35.75C35.0596 44.4893 34.5 43.9296 34.5 43.2393Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.3839 18.6161C45.872 19.1043 45.872 19.8957 45.3839 20.3839L37.8839 27.8839C37.3957 28.372 36.6043 28.372 36.1161 27.8839C35.628 27.3957 35.628 26.6043 36.1161 26.1161L43.6161 18.6161C44.1043 18.128 44.8957 18.128 45.3839 18.6161Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.1161 18.6161C36.6043 18.128 37.3957 18.128 37.8839 18.6161L45.3839 26.1161C45.872 26.6043 45.872 27.3957 45.3839 27.8839C44.8957 28.372 44.1043 28.372 43.6161 27.8839L36.1161 20.3839C35.628 19.8957 35.628 19.1043 36.1161 18.6161Z" fill="white"/>
</svg>
`;
5 changes: 1 addition & 4 deletions src/components/BaseFeedWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -89,7 +89,6 @@ const BaseFeedWidget = ({
<View style={styles.actions}>
<TouchableOpacity
style={styles.actionButton}
activeOpacity={0.7}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionDelete"
Expand All @@ -98,7 +97,6 @@ const BaseFeedWidget = ({
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
activeOpacity={0.7}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionEdit"
Expand All @@ -107,7 +105,6 @@ const BaseFeedWidget = ({
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
activeOpacity={0.7}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionDrag"
Expand Down
60 changes: 41 additions & 19 deletions src/components/Widgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
widgetsSelector,
} from '../store/reselect/widgets';
import { setWidgetsSortOrder } from '../store/slices/widgets';
import { TFeedWidget, TWidget } from '../store/types/widgets';
import { TFeedWidget } from '../store/types/widgets';
import { TouchableOpacity, View } from '../styles/components';
import { Checkmark, PlusIcon, SortAscendingIcon } from '../styles/icons';
import { Caption13Up } from '../styles/text';
Expand All @@ -33,10 +33,12 @@ import HeadlinesWidget from './HeadlinesWidget';
import LuganoFeedWidget from './LuganoFeedWidget';
import PriceWidget from './PriceWidget';
import Button from './buttons/Button';
import CalculatorWidget from './widgets/CalculatorWidget';

const Widgets = (): ReactElement => {
const { t } = useTranslation('slashtags');
const dispatch = useAppDispatch();

const widgets = useAppSelector(widgetsSelector);
const sortOrder = useAppSelector(widgetsOrderSelector);
const onboardedWidgets = useAppSelector(onboardedWidgetsSelector);
Expand All @@ -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],
Expand All @@ -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<string>): 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 (
<ScaleDecorator>
<Component
style={styles.widget}
isEditing={editing}
testID={testID}
onLongPress={initiateDrag}
onPressIn={initiateDrag}
/>
</ScaleDecorator>
);
}

const feedWidget = widgets[id] as TFeedWidget;

switch (feedWidget.type) {
case SUPPORTED_FEED_TYPES.PRICE_FEED:
Expand Down Expand Up @@ -117,17 +140,17 @@ const Widgets = (): ReactElement => {
<ScaleDecorator>
<Component
style={styles.widget}
url={url}
url={id}
widget={feedWidget}
isEditing={editing}
onLongPress={_drag}
onPressIn={_drag}
testID={testID}
onLongPress={initiateDrag}
onPressIn={initiateDrag}
/>
</ScaleDecorator>
);
},
[editing, sortedWidgets.length],
[editing, widgets, sortedWidgets.length],
);

return (
Expand All @@ -136,7 +159,6 @@ const Widgets = (): ReactElement => {
<Caption13Up color="secondary">{t('widgets')}</Caption13Up>
{sortedWidgets.length > 0 && (
<TouchableOpacity
activeOpacity={0.7}
hitSlop={{ top: 15, right: 15, bottom: 15, left: 15 }}
testID="WidgetsEdit"
onPress={(): void => setEditing(!editing)}>
Expand All @@ -151,7 +173,7 @@ const Widgets = (): ReactElement => {

<DraggableFlatList
data={sortedWidgets}
keyExtractor={(item): string => item[0]}
keyExtractor={(id): string => id}
renderItem={renderItem}
scrollEnabled={false}
activationDistance={editing ? 0 : 100}
Expand Down
176 changes: 176 additions & 0 deletions src/components/widgets/BaseWidget.tsx
Original file line number Diff line number Diff line change
@@ -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<ViewStyle>;
testID?: string;
onPress?: () => void;
onPressIn?: () => void;
onLongPress?: () => void;
}): ReactElement => {
const { t } = useTranslation('widgets');
const navigation = useNavigation<RootNavigationProp>();
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 (
<>
<TouchableOpacity
style={[styles.root, style]}
color="white10"
activeOpacity={0.9}
testID={testID}
onPress={onPress}
onPressIn={onPressIn}
onLongPress={onLongPress}>
{(showTitle || isEditing) && (
<View style={styles.header}>
<View style={styles.title}>
<View style={styles.icon}>
<SvgImage image={widget.icon} size={32} />
</View>

<BodyMSB style={styles.name} numberOfLines={1}>
{truncate(widget.name, 18)}
</BodyMSB>
</View>

{isEditing && (
<View style={styles.actions}>
<TouchableOpacity
style={styles.actionButton}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionDelete"
onPress={onDelete}>
<TrashIcon width={22} />
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionEdit"
onPress={onEdit}>
<SettingsIcon width={22} />
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
color="transparent"
hitSlop={{ top: 15, bottom: 15 }}
testID="WidgetActionDrag"
onLongPress={onLongPress}
onPressIn={onPressIn}>
<ListIcon color="white" width={24} />
</TouchableOpacity>
</View>
)}
</View>
)}

{showTitle && !isEditing && <View style={styles.spacer} />}

{!isEditing && children}
</TouchableOpacity>

<Dialog
visible={showDialog}
title={t('delete.title')}
description={t('delete.description', { name: widget.name })}
confirmText={t('delete_yes')}
onCancel={(): void => {
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);
Loading
Loading