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 = ({ >