mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-25 16:09:42 +01:00
mobile: new toast
This commit is contained in:
@@ -1,235 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eHideToast, eShowToast } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
export const Toast = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [data, setData] = useState({});
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const hideTimeout = useRef();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const toastMessages = useRef([]);
|
||||
|
||||
const show = useCallback(
|
||||
async (data) => {
|
||||
if (!data) return;
|
||||
if (data.context !== context) return;
|
||||
if (
|
||||
toastMessages.current.findIndex((m) => m.message === data.message) >= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
toastMessages.current.push(data);
|
||||
if (toastMessages.current?.length > 1) return;
|
||||
setData(data);
|
||||
|
||||
setVisible(true);
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hide();
|
||||
}, data.duration);
|
||||
},
|
||||
[context, hide]
|
||||
);
|
||||
|
||||
const next = useCallback(
|
||||
(data) => {
|
||||
if (!data) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
setData(data);
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hide();
|
||||
}, data?.duration);
|
||||
},
|
||||
[hide]
|
||||
);
|
||||
|
||||
const hide = useCallback(() => {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
let msg =
|
||||
toastMessages.current.length > 1 ? toastMessages.current.shift() : null;
|
||||
|
||||
if (msg) {
|
||||
setVisible(false);
|
||||
next(msg);
|
||||
setTimeout(() => {
|
||||
setVisible(true);
|
||||
}, 300);
|
||||
} else {
|
||||
setVisible(false);
|
||||
toastMessages.current.shift();
|
||||
setTimeout(() => {
|
||||
setData({});
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [next]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eShowToast, show);
|
||||
eSubscribeEvent(eHideToast, hide);
|
||||
return () => {
|
||||
toastMessages.current = [];
|
||||
eUnSubscribeEvent(eShowToast, show);
|
||||
eUnSubscribeEvent(eHideToast, hide);
|
||||
};
|
||||
}, [hide, show]);
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<TouchableOpacity
|
||||
onPress={hide}
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
width: DDS.isTab ? 400 : "100%",
|
||||
alignItems: "center",
|
||||
alignSelf: "center",
|
||||
minHeight: 30,
|
||||
top: insets.top + 10,
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
elevation: 15
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
maxWidth: "95%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
minWidth: data?.func ? "95%" : "50%",
|
||||
alignSelf: "center",
|
||||
borderRadius: 5,
|
||||
minHeight: 30,
|
||||
paddingVertical: 10,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 5,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "95%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexGrow: 1,
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 30,
|
||||
borderRadius: 100,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={data?.type === "success" ? "check" : "close"}
|
||||
size={SIZE.lg}
|
||||
color={
|
||||
data?.type === "error"
|
||||
? colors.error.icon
|
||||
: colors.primary.accent
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
paddingRight: 25
|
||||
}}
|
||||
>
|
||||
{data?.heading ? (
|
||||
<Heading
|
||||
color={colors.primary.paragraph}
|
||||
size={SIZE.md}
|
||||
onPress={() => {
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{data.heading}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{data?.message ? (
|
||||
<Paragraph
|
||||
color={colors.primary.paragraph}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
paddingRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{data.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{data.func ? (
|
||||
<Button
|
||||
testID={notesnook.toast.button}
|
||||
fontSize={SIZE.md}
|
||||
type={data.type === "error" ? "errorShade" : "transparent"}
|
||||
onPress={data.func}
|
||||
title={data.actionText}
|
||||
height={30}
|
||||
style={{
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
);
|
||||
};
|
||||
221
apps/mobile/app/components/toast/index.tsx
Normal file
221
apps/mobile/app/components/toast/index.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TouchableOpacity, useWindowDimensions, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastOptions
|
||||
} from "../../services/event-manager";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eHideToast, eShowToast } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const Toast = ({ context = "global" }) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const [toastOptions, setToastOptions] = useState<ToastOptions | undefined>(
|
||||
undefined
|
||||
);
|
||||
const hideTimeout = useRef<NodeJS.Timeout | undefined>();
|
||||
const keyboard = useKeyboard();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const toastMessages = useRef<ToastOptions[]>([]);
|
||||
const dimensions = useWindowDimensions();
|
||||
|
||||
const hideToast = useCallback(() => {
|
||||
const nextToastMessage = toastMessages.current.shift();
|
||||
if (nextToastMessage) {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
setVisible(true);
|
||||
setToastOptions(nextToastMessage);
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hideToast();
|
||||
}, nextToastMessage?.duration);
|
||||
} else {
|
||||
setVisible(false);
|
||||
setToastOptions(undefined);
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showToast = useCallback(
|
||||
(data?: ToastOptions) => {
|
||||
if (
|
||||
!data ||
|
||||
data.context !== context ||
|
||||
toastMessages.current.findIndex((m) => m.message === data.message) != -1
|
||||
)
|
||||
return;
|
||||
|
||||
toastMessages.current.push(data);
|
||||
if (toastMessages.current?.length > 1) return;
|
||||
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
setVisible(true);
|
||||
const nextToastMessage = toastMessages.current.shift();
|
||||
setToastOptions(nextToastMessage);
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hideToast();
|
||||
}, nextToastMessage?.duration);
|
||||
},
|
||||
[context, hideToast]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eShowToast, showToast);
|
||||
eSubscribeEvent(eHideToast, hideToast);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eShowToast, showToast);
|
||||
eUnSubscribeEvent(eHideToast, hideToast);
|
||||
};
|
||||
}, [hideToast, showToast]);
|
||||
|
||||
const isFullToastMessage = toastOptions?.heading && toastOptions?.message;
|
||||
|
||||
return visible && toastOptions ? (
|
||||
<TouchableOpacity
|
||||
onPress={hideToast}
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
width: DDS.isTab ? dimensions.width / 2 : "100%",
|
||||
alignItems: "center",
|
||||
alignSelf: "center",
|
||||
minHeight: 30,
|
||||
top: keyboard.keyboardShown
|
||||
? insets.top + 15
|
||||
: dimensions.height - Math.max(insets.bottom, 50),
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
elevation: 15
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
backgroundColor: isDark ? colors.static.black : colors.static.white,
|
||||
minWidth: toastOptions.func ? "95%" : "70%",
|
||||
alignSelf: "center",
|
||||
borderRadius: 100,
|
||||
minHeight: 30,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "75%",
|
||||
maxWidth: "95%",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexGrow: 1,
|
||||
flex: 1,
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
toastOptions.type === "success"
|
||||
? "check"
|
||||
: toastOptions.type === "info"
|
||||
? "information"
|
||||
: "close"
|
||||
}
|
||||
size={isFullToastMessage ? SIZE.xxxl : SIZE.xl}
|
||||
color={
|
||||
toastOptions?.icon
|
||||
? toastOptions?.icon
|
||||
: toastOptions.type === "error"
|
||||
? colors.error.icon
|
||||
: toastOptions.type === "info"
|
||||
? colors.static.white
|
||||
: colors.success.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
{isFullToastMessage ? (
|
||||
<Heading
|
||||
color={!isDark ? colors.static.black : colors.static.white}
|
||||
size={SIZE.sm}
|
||||
onPress={() => {
|
||||
hideToast();
|
||||
}}
|
||||
>
|
||||
{toastOptions.heading}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{toastOptions.message || toastOptions.heading ? (
|
||||
<Paragraph
|
||||
color={!isDark ? colors.static.black : colors.static.white}
|
||||
style={{
|
||||
paddingRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
hideToast();
|
||||
}}
|
||||
>
|
||||
{toastOptions.message || toastOptions.heading}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{toastOptions.func ? (
|
||||
<Button
|
||||
testID={notesnook.toast.button}
|
||||
fontSize={SIZE.md}
|
||||
type={toastOptions.type === "error" ? "errorShade" : "transparent"}
|
||||
onPress={toastOptions.func}
|
||||
title={toastOptions.actionText}
|
||||
height={30}
|
||||
style={{
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : null;
|
||||
};
|
||||
Reference in New Issue
Block a user