From a7312d0e7768c01b2b2497b0c5089b680738e4f0 Mon Sep 17 00:00:00 2001
From: duogenesis <136373989+duogenesis@users.noreply.github.com>
Date: Mon, 23 Dec 2024 21:38:55 +1100
Subject: [PATCH] Web UI refresh
---
App.tsx | 46 ++++--
components/{ => navigation}/tab-bar.tsx | 93 ++---------
components/navigation/util.tsx | 105 +++++++++++++
components/navigation/web-bar.tsx | 198 ++++++++++++++++++++++++
components/navigation/web-navigator.tsx | 147 ++++++++++++++++++
components/q-and-a-device.tsx | 5 +-
6 files changed, 498 insertions(+), 96 deletions(-)
rename components/{ => navigation}/tab-bar.tsx (63%)
create mode 100644 components/navigation/util.tsx
create mode 100644 components/navigation/web-bar.tsx
create mode 100644 components/navigation/web-navigator.tsx
diff --git a/App.tsx b/App.tsx
index ca4b2fc0..1a10bb33 100644
--- a/App.tsx
+++ b/App.tsx
@@ -26,7 +26,7 @@ import * as ScreenOrientation from 'expo-screen-orientation';
import * as SplashScreen from 'expo-splash-screen';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
-import { TabBar } from './components/tab-bar';
+import { TabBar } from './components/navigation/tab-bar';
import SearchTab from './components/search-tab';
import { QuizTab } from './components/quiz-tab';
import ProfileTab from './components/profile-tab';
@@ -58,6 +58,7 @@ import { ClubItem } from './club/club';
import { Toast } from './components/toast';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { DonationNagModal } from './components/donation-nag-modal';
+import { createWebNavigator } from './components/navigation/web-navigator';
setNofications();
verificationWatcher();
@@ -66,6 +67,7 @@ SplashScreen.preventAutoHideAsync();
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
+const WebNavigator = createWebNavigator();
if (
Platform.OS === 'android' &&
@@ -75,19 +77,35 @@ if (
}
const HomeTabs = () => {
- return (
- }
- >
-
-
-
-
-
-
- );
+ if (Platform.OS === 'web') {
+ return (
+ }
+ >
+
+
+
+
+
+
+ )
+ } else {
+ return (
+ }
+ >
+
+
+
+
+
+
+ );
+ }
};
const WebSplashScreen = ({loading}) => {
diff --git a/components/tab-bar.tsx b/components/navigation/tab-bar.tsx
similarity index 63%
rename from components/tab-bar.tsx
rename to components/navigation/tab-bar.tsx
index 00d2de79..d44a6f64 100644
--- a/components/tab-bar.tsx
+++ b/components/navigation/tab-bar.tsx
@@ -8,21 +8,17 @@ import {
useEffect,
useRef,
} from 'react';
-import { DefaultText } from './default-text';
+import { DefaultText } from '../default-text';
import Ionicons from '@expo/vector-icons/Ionicons';
import { StackActions } from '@react-navigation/native';
-import { QAndADevice } from './q-and-a-device';
-import { Inbox, inboxStats } from '../xmpp/xmpp';
+import { QAndADevice } from '../q-and-a-device';
+import { Inbox, inboxStats } from '../../xmpp/xmpp';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { listen } from '../events/events';
-
-const displayedTabs: Set = new Set([
- "Q&A",
- "Search",
- "Inbox",
- "Traits",
- "Profile",
-]);
+import { listen } from '../../events/events';
+import {
+ LabelToIcon,
+ displayedTabs
+} from './util';
const TabBar = ({state, descriptors, navigation}) => {
const insets = useSafeAreaInsets();
@@ -136,17 +132,6 @@ const TabBar = ({state, descriptors, navigation}) => {
const inputRange = state.routes.map((_, i) => i);
- const searchIcon =
- isFocused ? 'search' : 'search-outline';
- const inboxIcon =
- isFocused ? 'chatbubbles' : 'chatbubbles-outline';
- const profileIcon =
- isFocused ? 'person' : 'person-outline';
-
- const iconStyle = {
- fontSize: 20,
- };
-
return [
{
overflow: 'visible',
}}
>
- {label === 'Q&A' &&
-
- }
-
- {label === 'Search' &&
-
- }
- {label === 'Inbox' &&
-
- }
- {label === 'Inbox' &&
-
- }
- {label === 'Traits' &&
-
-
- Ψ
-
-
- }
- {label === 'Profile' &&
-
- }
-
+
= new Set([
+ "Q&A",
+ "Search",
+ "Inbox",
+ "Traits",
+ "Profile",
+]);
+
+const LabelToIcon = ({
+ label,
+ isFocused,
+ unreadIndicatorOpacity,
+ color = "black",
+ backgroundColor = undefined,
+ fontSize = 20,
+}) => {
+ const searchIcon =
+ isFocused ? 'search' : 'search-outline';
+ const inboxIcon =
+ isFocused ? 'chatbubbles' : 'chatbubbles-outline';
+ const profileIcon =
+ isFocused ? 'person' : 'person-outline';
+
+ const iconStyle = {
+ fontSize: fontSize,
+ color: color,
+ };
+
+ return (
+ <>
+ {label === 'Q&A' &&
+
+ }
+
+ {label === 'Search' &&
+
+ }
+ {label === 'Inbox' &&
+
+ }
+ {label === 'Inbox' &&
+
+ }
+ {label === 'Traits' &&
+
+
+ Ψ
+
+
+ }
+ {label === 'Profile' &&
+
+ }
+
+ >
+ );
+};
+
+export {
+ displayedTabs,
+ LabelToIcon,
+}
diff --git a/components/navigation/web-bar.tsx b/components/navigation/web-bar.tsx
new file mode 100644
index 00000000..c3e197a8
--- /dev/null
+++ b/components/navigation/web-bar.tsx
@@ -0,0 +1,198 @@
+import {
+ Animated,
+ Pressable,
+ Text,
+ View,
+} from 'react-native';
+import {
+ useCallback,
+ useEffect,
+ useRef,
+} from 'react';
+import {
+ DefaultText,
+} from '../default-text';
+import {
+ CommonActions,
+} from '@react-navigation/native';
+import {
+ Logo16
+} from '../logo';
+import {
+ LabelToIcon
+} from './util';
+import { Inbox, inboxStats } from '../../xmpp/xmpp';
+import { listen } from '../../events/events';
+
+const Logo = () => {
+ return (
+
+
+
+
+
+ Duolicious
+
+
+ );
+};
+
+const NavigationItems = ({state, navigation, descriptors}) => {
+ const prevNumUnread = useRef(0);
+ const numUnread = useRef(0);
+
+ const unreadIndicatorOpacity = useRef(new Animated.Value(0)).current;
+
+ const hideIndicator = useCallback(() => {
+ unreadIndicatorOpacity.setValue(0);
+ }, []);
+
+ const showIndicator = useCallback(() => {
+ unreadIndicatorOpacity.setValue(1);
+ }, []);
+
+ const onChangeInbox = useCallback((inbox: Inbox | null) => {
+ if (inbox) {
+ prevNumUnread.current = numUnread.current;
+
+ const stats = inboxStats(inbox);
+ numUnread.current = stats.numChats ?
+ stats.numUnreadChats :
+ stats.numUnreadIntros;
+
+ } else {
+ prevNumUnread.current = numUnread.current;
+ numUnread.current = 0;
+ }
+
+ if (numUnread.current === 0) {
+ hideIndicator();
+ } else if (numUnread.current > prevNumUnread.current) {
+ showIndicator();
+ }
+ }, []);
+
+ useEffect(() => {
+ return listen('inbox', onChangeInbox, true);
+ }, []);
+
+ return (
+
+ {state.routes.map((route, index) => {
+ const { options } = descriptors[route.key];
+ const label =
+ options.tabBarLabel !== undefined
+ ? options.tabBarLabel
+ : options.title !== undefined
+ ? options.title
+ : route.name;
+
+ const isFocused = state.index === index;
+
+ return (
+ {
+ const isFocused = state.index === index;
+ const event = navigation.emit({
+ type: 'tabPress',
+ target: route.key,
+ canPreventDefault: true,
+ data: {
+ isAlreadyFocused: isFocused,
+ },
+ });
+
+ if (!isFocused && !event.defaultPrevented) {
+ navigation.dispatch({
+ ...CommonActions.navigate(route),
+ target: state.key,
+ });
+ }
+ }}
+ style={{
+ padding: 16,
+ flexDirection: 'row',
+ gap: 10,
+ }}
+ >
+
+
+
+
+ {descriptors[route.key].options.title || route.name}
+
+
+ );
+ })}
+
+ );
+};
+
+const WebBar = ({state, navigation, tabBarStyle, descriptors}) => {
+ return (
+
+
+
+
+ );
+};
+
+export {
+ WebBar,
+}
diff --git a/components/navigation/web-navigator.tsx b/components/navigation/web-navigator.tsx
new file mode 100644
index 00000000..ca35f5f4
--- /dev/null
+++ b/components/navigation/web-navigator.tsx
@@ -0,0 +1,147 @@
+import {
+ View,
+ Text,
+ Pressable,
+ type StyleProp,
+ type ViewStyle,
+ StyleSheet,
+} from 'react-native';
+import {
+ createNavigatorFactory,
+ type DefaultNavigatorOptions,
+ type NavigatorTypeBagBase,
+ type ParamListBase,
+ type StaticConfig,
+ type TabActionHelpers,
+ type TabNavigationState,
+ TabRouter,
+ type TabRouterOptions,
+ type TypedNavigator,
+ useNavigationBuilder,
+} from '@react-navigation/native';
+import { WebBar } from './web-bar';
+
+// Props accepted by the view
+type TabNavigationConfig = {
+ tabBarStyle: StyleProp;
+ contentStyle: StyleProp;
+};
+
+// Supported screen options
+type TabNavigationOptions = {
+ title?: string;
+};
+
+// Map of event name and the type of data (in event.data)
+//
+// canPreventDefault: true adds the defaultPrevented property to the
+// emitted events.
+type TabNavigationEventMap = {
+ tabPress: {
+ data: { isAlreadyFocused: boolean };
+ canPreventDefault: true;
+ };
+};
+
+// The props accepted by the component is a combination of 3 things
+type Props = DefaultNavigatorOptions<
+ ParamListBase,
+ string | undefined,
+ TabNavigationState,
+ TabNavigationOptions,
+ TabNavigationEventMap,
+ Navigation
+> &
+ TabRouterOptions &
+ TabNavigationConfig;
+
+function WebNavigator({
+ id,
+ initialRouteName,
+ children,
+ layout,
+ screenListeners,
+ screenOptions,
+ screenLayout,
+ backBehavior,
+ tabBarStyle,
+ contentStyle,
+}: Props) {
+ const { state, navigation, descriptors, NavigationContent } =
+ useNavigationBuilder<
+ TabNavigationState,
+ TabRouterOptions,
+ TabActionHelpers,
+ TabNavigationOptions,
+ TabNavigationEventMap
+ >(TabRouter, {
+ id,
+ initialRouteName,
+ children,
+ layout,
+ screenListeners,
+ screenOptions,
+ screenLayout,
+ backBehavior,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+ {state.routes.map((route, i) => {
+ return (
+
+ {descriptors[route.key].render()}
+
+ );
+ })}
+
+
+
+
+ );
+};
+
+function createWebNavigator(config?: any) {
+ return createNavigatorFactory(WebNavigator)(config);
+}
+
+export {
+ createWebNavigator,
+};
diff --git a/components/q-and-a-device.tsx b/components/q-and-a-device.tsx
index 616fb7d6..e4424026 100644
--- a/components/q-and-a-device.tsx
+++ b/components/q-and-a-device.tsx
@@ -9,6 +9,7 @@ const QAndADevice = ({
fontSize = 20,
isBold = false,
spacing = -6,
+ backgroundColor = 'white',
}) => {
const noIcon = isBold ? 'close-circle' : 'close-circle-outline';
const yesIcon = isBold ? 'checkmark-circle' : 'checkmark-circle-outline';
@@ -22,7 +23,7 @@ const QAndADevice = ({
>