Skip to content

Commit c4e9022

Browse files
committed
feat(widgets): add caching
1 parent cbaa788 commit c4e9022

16 files changed

+208
-78
lines changed

ReactotronConfig.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Reactotron from 'reactotron-react-native';
22
import { reactotronRedux } from 'reactotron-redux';
33
import mmkvPlugin from 'reactotron-react-native-mmkv';
4-
import { storage } from './src/store/mmkv-storage';
4+
import { storage } from './src/storage';
55

66
const reactotron = Reactotron.configure()
77
.use(reactotronRedux())

src/components/SlashtagsProvider.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React, { ReactElement, useEffect, useState } from 'react';
77
import { createContext } from 'react';
88

99
import { useAppSelector } from '../hooks/redux';
10-
import { WebRelayCache } from '../store/mmkv-storage';
10+
import { WebRelayCache } from '../storage';
1111
import { webRelaySelector } from '../store/reselect/settings';
1212
import { seedHashSelector } from '../store/reselect/wallet';
1313
import i18n from '../utils/i18n';

src/hooks/useBlocksWidget.ts

+21-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useEffect, useState } from 'react';
22
import { __E2E__ } from '../constants/env';
3+
import { widgetsCache } from '../storage/widgets-cache';
34
import { i18nTime } from '../utils/i18n';
45

5-
type TBlocksWidgetData = {
6+
type TWidgetData = {
67
height: string;
78
time: string;
89
date: string;
@@ -32,15 +33,24 @@ type ErrorState = {
3233

3334
type ReadyState = {
3435
status: EWidgetStatus.Ready;
35-
data: TBlocksWidgetData;
36+
data: TWidgetData;
3637
};
3738

3839
type TWidgetState = LoadingState | ErrorState | ReadyState;
3940

4041
const BASE_URL = 'https://mempool.space/api';
4142
const REFRESH_INTERVAL = 1000 * 60 * 2; // 2 minutes
43+
const CACHE_KEY = 'blocks';
4244

43-
const formatBlockInfo = (blockInfo): TBlocksWidgetData => {
45+
const cacheData = (data: TWidgetData) => {
46+
widgetsCache.set(CACHE_KEY, data);
47+
};
48+
49+
const getCachedData = (): TWidgetData | null => {
50+
return widgetsCache.get<TWidgetData>(CACHE_KEY);
51+
};
52+
53+
const formatBlockInfo = (blockInfo): TWidgetData => {
4454
const { format } = new Intl.NumberFormat('en-US');
4555

4656
const difficulty = (blockInfo.difficulty / 1000000000000).toFixed(2);
@@ -88,9 +98,11 @@ const formatBlockInfo = (blockInfo): TBlocksWidgetData => {
8898
};
8999

90100
const useBlocksWidget = (): TWidgetState => {
91-
const [state, setState] = useState<TWidgetState>({
92-
status: EWidgetStatus.Loading,
93-
data: null,
101+
const [state, setState] = useState<TWidgetState>(() => {
102+
const cached = getCachedData();
103+
return cached
104+
? { status: EWidgetStatus.Ready, data: cached }
105+
: { status: EWidgetStatus.Loading, data: null };
94106
});
95107

96108
useEffect(() => {
@@ -122,9 +134,10 @@ const useBlocksWidget = (): TWidgetState => {
122134
try {
123135
const hash = await fetchTipHash();
124136
const blockInfo = await fetchBlockInfo(hash);
125-
const formatted = formatBlockInfo(blockInfo);
137+
const data = formatBlockInfo(blockInfo);
126138

127-
setState({ status: EWidgetStatus.Ready, data: formatted });
139+
cacheData(data);
140+
setState({ status: EWidgetStatus.Ready, data });
128141
} catch (error) {
129142
console.error('Failed to fetch block data:', error);
130143
setState({ status: EWidgetStatus.Error, data: null });

src/hooks/useNewsWidget.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useState } from 'react';
22
import { __E2E__ } from '../constants/env';
3+
import { widgetsCache } from '../storage/widgets-cache';
34
import { timeAgo } from '../utils/helpers';
45

56
type TArticle = {
@@ -18,7 +19,7 @@ type TArticle = {
1819
};
1920
};
2021

21-
type TWidgetArticle = {
22+
type TWidgetData = {
2223
title: string;
2324
timeAgo: string;
2425
link: string;
@@ -43,18 +44,29 @@ type ErrorState = {
4344

4445
type ReadyState = {
4546
status: EWidgetStatus.Ready;
46-
data: TWidgetArticle;
47+
data: TWidgetData;
4748
};
4849

4950
type TWidgetState = LoadingState | ErrorState | ReadyState;
5051

5152
const BASE_URL = 'https://feeds.synonym.to/news-feed/api';
5253
const REFRESH_INTERVAL = 1000 * 60 * 2; // 2 minutes
54+
const CACHE_KEY = 'news';
55+
56+
const cacheData = (data: TWidgetData) => {
57+
widgetsCache.set(CACHE_KEY, data);
58+
};
59+
60+
const getCachedData = (): TWidgetData | null => {
61+
return widgetsCache.get<TWidgetData>(CACHE_KEY);
62+
};
5363

5464
const useNewsWidget = (): TWidgetState => {
55-
const [state, setState] = useState<TWidgetState>({
56-
status: EWidgetStatus.Loading,
57-
data: null,
65+
const [state, setState] = useState<TWidgetState>(() => {
66+
const cached = getCachedData();
67+
return cached
68+
? { status: EWidgetStatus.Ready, data: cached }
69+
: { status: EWidgetStatus.Loading, data: null };
5870
});
5971

6072
useEffect(() => {
@@ -71,7 +83,6 @@ const useNewsWidget = (): TWidgetState => {
7183
};
7284

7385
const fetchData = async (): Promise<void> => {
74-
setState({ status: EWidgetStatus.Loading, data: null });
7586
try {
7687
const articles = await fetchArticles();
7788

@@ -87,6 +98,7 @@ const useNewsWidget = (): TWidgetState => {
8798
publisher: article.publisher.title,
8899
};
89100

101+
cacheData(data);
90102
setState({ status: EWidgetStatus.Ready, data });
91103
} catch (error) {
92104
console.error('Failed to fetch news data:', error);

src/hooks/usePriceWidget.ts

+43-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useState } from 'react';
22
import { __E2E__ } from '../constants/env';
33
import { tradingPairs } from '../constants/widgets';
4+
import { widgetsCache } from '../storage/widgets-cache';
45
import { TGraphPeriod } from '../store/types/widgets';
56
import { IThemeColors } from '../styles/themes';
67

@@ -94,13 +95,42 @@ export const formatPrice = (pair: TradingPair, price: number): string => {
9495
}
9596
};
9697

98+
const cacheData = (
99+
pairName: string,
100+
period: TGraphPeriod,
101+
data: TWidgetData,
102+
) => {
103+
const cacheKey = `${pairName}_${period}`;
104+
widgetsCache.set(cacheKey, data);
105+
};
106+
107+
const getCachedData = (
108+
pairs: string[],
109+
period: TGraphPeriod,
110+
): TWidgetData[] | null => {
111+
const data = pairs.map((pairName) => {
112+
const cacheKey = `${pairName}_${period}`;
113+
const cached = widgetsCache.get<TWidgetData>(cacheKey);
114+
return cached;
115+
});
116+
117+
const allCached = data.every((d) => d !== null);
118+
if (allCached) {
119+
return data;
120+
}
121+
122+
return null;
123+
};
124+
97125
const usePriceWidget = (
98126
pairs: string[],
99127
period: TGraphPeriod,
100128
): TWidgetState => {
101-
const [state, setState] = useState<TWidgetState>({
102-
status: EWidgetStatus.Loading,
103-
data: null,
129+
const [state, setState] = useState<TWidgetState>(() => {
130+
const cached = getCachedData(pairs, period);
131+
return cached
132+
? { status: EWidgetStatus.Ready, data: cached }
133+
: { status: EWidgetStatus.Loading, data: null };
104134
});
105135

106136
// biome-ignore lint/correctness/useExhaustiveDependencies: pairs is an array so deep check it
@@ -146,12 +176,16 @@ const usePriceWidget = (
146176
const change = getChange(updatedPastValues);
147177
const price = formatPrice(pair, latestPrice);
148178

149-
return {
179+
const data = {
150180
name: pairName,
151181
price,
152182
change,
153183
pastValues: updatedPastValues,
154184
};
185+
186+
cacheData(pairName, period, data);
187+
188+
return data;
155189
});
156190
const data = await Promise.all(promises);
157191
setState({ status: EWidgetStatus.Ready, data });
@@ -179,12 +213,16 @@ const usePriceWidget = (
179213
const change = getChange(newPastValues);
180214
const price = formatPrice(pair, latestPrice);
181215

182-
return {
216+
const data = {
183217
...pairData,
184218
price,
185219
change,
186220
pastValues: newPastValues,
187221
};
222+
223+
cacheData(pairData.name, period, data);
224+
225+
return data;
188226
}),
189227
);
190228
setState({ status: EWidgetStatus.Ready, data: updatedData });

src/hooks/useWeatherWidget.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useState } from 'react';
22
import { __E2E__ } from '../constants/env';
3+
import { widgetsCache } from '../storage/widgets-cache';
34
import { refreshOnchainFeeEstimates } from '../store/utils/fees';
45
import { getDisplayValues, getFiatDisplayValues } from '../utils/displayValues';
56

@@ -56,6 +57,15 @@ const VBYTES_SIZE = 140; // average native segwit transaction size
5657
const USD_GOOD_THRESHOLD = 1; // $1 USD threshold for good condition
5758
const PERCENTILE_LOW = 0.33;
5859
const PERCENTILE_HIGH = 0.66;
60+
const CACHE_KEY = 'weather';
61+
62+
const cacheData = (data: TWidgetData) => {
63+
widgetsCache.set(CACHE_KEY, data);
64+
};
65+
66+
const getCachedData = (): TWidgetData | null => {
67+
return widgetsCache.get<TWidgetData>(CACHE_KEY);
68+
};
5969

6070
const calculateCondition = (
6171
currentFeeRate: number,
@@ -97,9 +107,11 @@ const calculateCondition = (
97107
};
98108

99109
const useWeatherWidget = (): TWidgetState => {
100-
const [state, setState] = useState<TWidgetState>({
101-
status: EWidgetStatus.Loading,
102-
data: null,
110+
const [state, setState] = useState<TWidgetState>(() => {
111+
const cached = getCachedData();
112+
return cached
113+
? { status: EWidgetStatus.Ready, data: cached }
114+
: { status: EWidgetStatus.Loading, data: null };
103115
});
104116

105117
useEffect(() => {
@@ -137,6 +149,7 @@ const useWeatherWidget = (): TWidgetState => {
137149
const currentFee = `${dv.fiatSymbol} ${dv.fiatFormatted}`;
138150
const data = { condition, currentFee, nextBlockFee: fees.fast };
139151

152+
cacheData(data);
140153
setState({ status: EWidgetStatus.Ready, data });
141154
} catch (error) {
142155
console.error('Failed to fetch fee data:', error);

src/screens/Settings/DevSettings/index.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import { EItemType, IListData } from '../../../components/List';
88
import { __E2E__ } from '../../../constants/env';
99
import { useAppDispatch, useAppSelector } from '../../../hooks/redux';
1010
import type { SettingsScreenProps } from '../../../navigation/types';
11+
import { widgetsCache } from '../../../storage';
12+
import { storage } from '../../../storage';
1113
import actions from '../../../store/actions/actions';
1214
import {
1315
clearUtxos,
1416
injectFakeTransaction,
1517
} from '../../../store/actions/wallet';
1618
import { getStore, getWalletStore } from '../../../store/helpers';
17-
import { storage } from '../../../store/mmkv-storage';
1819
import { warningsSelector } from '../../../store/reselect/checks';
1920
import { settingsSelector } from '../../../store/reselect/settings';
2021
import {
@@ -171,6 +172,11 @@ const DevSettings = ({
171172
type: EItemType.button,
172173
onPress: clearWebRelayCache,
173174
},
175+
{
176+
title: 'Clear Widgets Cache',
177+
type: EItemType.button,
178+
onPress: widgetsCache.clear,
179+
},
174180
{
175181
title: "Clear UTXO's",
176182
type: EItemType.button,

src/storage/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { MMKV } from 'react-native-mmkv';
2+
import { receivedTxIds } from './received-tx-cache';
3+
import { reduxStorage } from './redux-storage';
4+
import { WebRelayCache } from './webrelay-cache';
5+
import { widgetsCache } from './widgets-cache';
6+
7+
export const storage = new MMKV();
8+
9+
export { reduxStorage, receivedTxIds, WebRelayCache, widgetsCache };

src/storage/received-tx-cache.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Used to prevent duplicate notifications for the same txId that seems to occur when:
2+
// - when Bitkit is brought from background to foreground
3+
4+
import { storage } from '.';
5+
6+
// - connection to electrum server is lost and then re-established
7+
export const receivedTxIds = {
8+
STORAGE_KEY: 'receivedTxIds',
9+
10+
// Get stored txIds
11+
get: (): string[] => {
12+
return JSON.parse(storage.getString(receivedTxIds.STORAGE_KEY) || '[]');
13+
},
14+
15+
// Save txIds to storage
16+
save: (txIds: string[]): void => {
17+
storage.set(receivedTxIds.STORAGE_KEY, JSON.stringify(txIds));
18+
},
19+
20+
// Add a new txId
21+
add: (txId: string): void => {
22+
const txIds = receivedTxIds.get();
23+
txIds.push(txId);
24+
receivedTxIds.save(txIds);
25+
},
26+
27+
// Check if txId exists
28+
has: (txId: string): boolean => {
29+
const txIds = receivedTxIds.get();
30+
return txIds.includes(txId);
31+
},
32+
};

src/storage/redux-storage.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Storage } from 'redux-persist';
2+
import { storage } from '.';
3+
4+
export const reduxStorage: Storage = {
5+
setItem: (key, value): Promise<boolean> => {
6+
storage.set(key, value);
7+
return Promise.resolve(true);
8+
},
9+
getItem: (key): Promise<string | undefined> => {
10+
const value = storage.getString(key);
11+
return Promise.resolve(value);
12+
},
13+
removeItem: (key): Promise<void> => {
14+
storage.delete(key);
15+
return Promise.resolve();
16+
},
17+
};

0 commit comments

Comments
 (0)