Skip to content

Commit

Permalink
feat(apps/mobile): add toasts for all user-surfaceable actions
Browse files Browse the repository at this point in the history
  • Loading branch information
hassankhan committed Oct 13, 2024
1 parent 4af59da commit 9023d1d
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 11 deletions.
5 changes: 3 additions & 2 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@
"expo-image": "*",
"redux-devtools-expo-dev-plugin": "*",
"@gorhom/bottom-sheet": "*",
"ts-pattern": "*"
"ts-pattern": "*",
"react-native-toast-message": "*"
},
"scripts": {
"eas-build-post-install": "cd ../../ && node tools/scripts/eas-build-post-install.mjs . apps/mobile",
"android": "expo run:android",
"ios": "expo run:ios"
},
"devDependencies": {}
}
}
18 changes: 15 additions & 3 deletions apps/mobile/src/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Stack } from 'expo-router/stack';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import Toast from 'react-native-toast-message';
import { Provider } from 'react-redux';
import { CustomToast } from '../components/CustomToast';
import { store } from '../store/store';

const toastConfig = {
success: CustomToast,
error: CustomToast,
};

const NavigationLayout = () => {
return (
<Stack>
Expand All @@ -12,9 +20,13 @@ const NavigationLayout = () => {

const ProviderLayout = () => {
return (
<Provider store={store}>
<NavigationLayout />
</Provider>
<GestureHandlerRootView>
<Provider store={store}>
<NavigationLayout />
<Toast config={toastConfig} />
{/*<Toast />*/}
</Provider>
</GestureHandlerRootView>
);
};

Expand Down
5 changes: 2 additions & 3 deletions apps/mobile/src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useState } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { createStyleSheet, useStyles } from 'react-native-unistyles';
import { match, P } from 'ts-pattern';

Expand All @@ -27,7 +26,7 @@ const Home = () => {
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);

return (
<GestureHandlerRootView style={styles.container}>
<View style={styles.container}>
{match({ isLoading, images })
.with({ isLoading: true, images: P.any }, () => (
<ActivityIndicator size="large" color="blue" />
Expand All @@ -50,7 +49,7 @@ const Home = () => {
setIsBottomSheetOpen(false);
}}
/>
</GestureHandlerRootView>
</View>
);
};

Expand Down
16 changes: 16 additions & 0 deletions apps/mobile/src/components/CustomToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ComponentProps } from 'react';
import { BaseToast } from 'react-native-toast-message';

export const CustomToast = (props: ComponentProps<typeof BaseToast>) => {
return (
<BaseToast
{...props}
style={{ borderLeftColor: 'transparent' }}
contentContainerStyle={{ paddingHorizontal: 15 }}
text1Style={{
fontSize: 15,
fontWeight: '400',
}}
/>
);
};
178 changes: 178 additions & 0 deletions apps/mobile/src/store/middleware/ToastMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
import { CatApi } from '../services/CatApi';
import { AppDispatch, RootState } from '../store';
import { showToast } from '../thunks/showToast';

export const ToastMiddleware = createListenerMiddleware();

export const startAppListening = ToastMiddleware.startListening.withTypes<
RootState,
AppDispatch
>();

export const addAppListener = addListener.withTypes<RootState, AppDispatch>();

const TITLE_SUCCESS = 'Success';
const TITLE_ERROR = 'Error';

startAppListening({
matcher: CatApi.endpoints.uploadImage.matchFulfilled,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `🩻 ${TITLE_SUCCESS}`,
message: 'Image uploaded successfully',
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.uploadImage.matchRejected,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `🩻 ${TITLE_ERROR}`,
message: [
'There was an error while uploading your image',
action.error.message,
].join('\n'),
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.deleteImage.matchFulfilled,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `🩻 ${TITLE_SUCCESS}`,
message: 'Image deleted successfully',
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.deleteImage.matchRejected,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `🩻 ${TITLE_ERROR}`,
message: [
'There was an error while deleting your image',
action.error.message,
].join('\n'),
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.upvoteImage.matchFulfilled,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `🔼 ${TITLE_SUCCESS}`,
message: 'Your upvote was successful',
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.upvoteImage.matchRejected,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `🔼 ${TITLE_ERROR}`,
message: [
'There was an error while sending your upvote',
action.error.message,
].join('\n'),
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.downvoteImage.matchFulfilled,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `🔽 ${TITLE_SUCCESS}`,
message: 'Your downvote was successful',
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.downvoteImage.matchRejected,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `🔼 ${TITLE_ERROR}`,
message: [
'There was an error while sending your downvote',
action.error.message,
].join('\n'),
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.favouriteImage.matchFulfilled,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `❤️ ${TITLE_SUCCESS}`,
message: 'Favourited the kitty successfully',
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.favouriteImage.matchRejected,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `❤️ ${TITLE_ERROR}`,
message: [
'There was an error while favouriting the image',
action.error.message,
].join('\n'),
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.unfavouriteImage.matchFulfilled,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `💔 ${TITLE_SUCCESS}`,
message: 'Image unfavourited successfully',
})
);
},
});

startAppListening({
matcher: CatApi.endpoints.unfavouriteImage.matchRejected,
effect: async (action, { dispatch }) => {
await dispatch(
showToast({
title: `❤️ ${TITLE_ERROR}`,
message: [
'There was an error while unfavouriting the image',
action.error.message,
].join('\n'),
})
);
},
});
8 changes: 5 additions & 3 deletions apps/mobile/src/store/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import devToolsEnhancer from 'redux-devtools-expo-dev-plugin';
import { ToastMiddleware } from './middleware/ToastMiddleware';

import { CatApi } from './services/CatApi';

Expand All @@ -10,9 +11,10 @@ export const store = configureStore({
? getDefaultEnhancers().concat(devToolsEnhancer())
: getDefaultEnhancers(),
middleware: (getDefaultMiddleware) => {
return getDefaultMiddleware().concat(
CatApi.middleware
) as unknown as ReturnType<typeof getDefaultMiddleware>;
type DefaultMiddleware = ReturnType<typeof getDefaultMiddleware>;
return getDefaultMiddleware()
.prepend(ToastMiddleware.middleware)
.concat(CatApi.middleware) as unknown as DefaultMiddleware;
},
reducer: {
// RTK-Query reducers
Expand Down
19 changes: 19 additions & 0 deletions apps/mobile/src/store/thunks/showToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Toast from 'react-native-toast-message';
import { createAppAsyncThunk } from '../overrides';

type ShowToastOptions = {
title: string;
message: string;
};

export const showToast = createAppAsyncThunk(
'app/showToast',
async ({ title, message }: ShowToastOptions) => {
Toast.show({
topOffset: 60,
type: 'success',
text1: title,
text2: message,
});
}
);
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react-native-screens": "3.31.1",
"react-native-svg": "15.2.0",
"react-native-svg-transformer": "1.3.0",
"react-native-toast-message": "^2.2.1",
"react-native-unistyles": "^2.10.0",
"react-native-web": "~0.19.11",
"react-redux": "^9.1.2",
Expand Down

0 comments on commit 9023d1d

Please sign in to comment.