From 1e2ee001a8ea8c5e12de480c900e6d92ea14ae7c Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 6 Dec 2022 10:11:32 -0600 Subject: [PATCH] Rework the 'main menu' to be a screen that's always in history --- src/state/models/navigation.ts | 19 +- src/state/models/session.ts | 5 +- src/view/lib/icons.tsx | 32 +++ src/view/routes.ts | 2 + src/view/screens/Menu.tsx | 246 ++++++++++++++++++++ src/view/shell/mobile/MainMenu.tsx | 354 ----------------------------- src/view/shell/mobile/index.tsx | 21 +- 7 files changed, 300 insertions(+), 379 deletions(-) create mode 100644 src/view/screens/Menu.tsx delete mode 100644 src/view/shell/mobile/MainMenu.tsx diff --git a/src/state/models/navigation.ts b/src/state/models/navigation.ts index 6eb13a62ae..93aab9d4f1 100644 --- a/src/state/models/navigation.ts +++ b/src/state/models/navigation.ts @@ -17,8 +17,11 @@ export type HistoryPtr = [number, number] export class NavigationTabModel { id = genId() - history: HistoryItem[] = [{url: '/', ts: Date.now(), id: genId()}] - index = 0 + history: HistoryItem[] = [ + {url: '/menu', ts: Date.now(), id: genId()}, + {url: '/', ts: Date.now(), id: genId()}, + ] + index = 1 isNewTab = false constructor() { @@ -107,9 +110,15 @@ export class NavigationTabModel { } } - goBackToZero() { - if (this.canGoBack) { - this.index = 0 + resetTo(path: string) { + if (this.index >= 1 && this.history[1]?.url === path) { + // fall back in history to target + if (this.index > 1) { + this.index = 1 + } + } else { + this.history = [this.history[0], {url: path, ts: Date.now(), id: genId()}] + this.index = 1 } } diff --git a/src/state/models/session.ts b/src/state/models/session.ts index 069e3db32b..1537d13166 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -138,7 +138,10 @@ export class SessionModel { } async connect(): Promise { - this._connectPromise ??= this._connect() + if (this._connectPromise) { + return this._connectPromise + } + this._connectPromise = this._connect() await this._connectPromise this._connectPromise = undefined } diff --git a/src/view/lib/icons.tsx b/src/view/lib/icons.tsx index 7e3313597c..31869810d0 100644 --- a/src/view/lib/icons.tsx +++ b/src/view/lib/icons.tsx @@ -166,6 +166,38 @@ export function BellIconSolid({ ) } +export function CogIcon({ + style, + size, + strokeWidth = 1.5, +}: { + style?: StyleProp + size?: string | number + strokeWidth: number +}) { + return ( + + + + + ) +} + // Copyright (c) 2020 Refactoring UI Inc. // https://github.com/tailwindlabs/heroicons/blob/master/LICENSE export function UserGroupIcon({ diff --git a/src/view/routes.ts b/src/view/routes.ts index 272a1b096d..e662e2cca7 100644 --- a/src/view/routes.ts +++ b/src/view/routes.ts @@ -1,6 +1,7 @@ import React, {MutableRefObject} from 'react' import {FlatList} from 'react-native' import {IconProp} from '@fortawesome/fontawesome-svg-core' +import {Menu} from './screens/Menu' import {Home} from './screens/Home' import {Contacts} from './screens/Contacts' import {Search} from './screens/Search' @@ -33,6 +34,7 @@ export type MatchResult = { const r = (pattern: string) => new RegExp('^' + pattern + '([?]|$)', 'i') export const routes: Route[] = [ + [Menu, 'Menu', 'bars', r('/menu')], [Home, 'Home', 'house', r('/')], [Contacts, 'Contacts', ['far', 'circle-user'], r('/contacts')], [Search, 'Search', 'magnifying-glass', r('/search')], diff --git a/src/view/screens/Menu.tsx b/src/view/screens/Menu.tsx new file mode 100644 index 0000000000..d226ea02b0 --- /dev/null +++ b/src/view/screens/Menu.tsx @@ -0,0 +1,246 @@ +import React, {useEffect} from 'react' +import { + StyleProp, + StyleSheet, + Text, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native' +import {colors} from '../lib/styles' +import {ScreenParams} from '../routes' +import {useStores} from '../../state' +import { + HomeIcon, + UserGroupIcon, + BellIcon, + CogIcon, + MagnifyingGlassIcon, +} from '../lib/icons' +import {UserAvatar} from '../com/util/UserAvatar' +import {ViewHeader} from '../com/util/ViewHeader' +import {CreateSceneModel} from '../../state/models/shell-ui' + +export const Menu = ({navIdx, visible}: ScreenParams) => { + const store = useStores() + + useEffect(() => { + if (visible) { + store.nav.setTitle(navIdx, 'Menu') + // trigger a refresh in case memberships have changed recently + store.me.refreshMemberships() + } + }, [store, visible]) + + // events + // = + + const onNavigate = (url: string) => { + store.nav.navigate(url) + } + const onPressCreateScene = () => { + store.shell.openModal(new CreateSceneModel()) + } + + // rendering + // = + + const MenuItem = ({ + icon, + label, + count, + url, + bold, + onPress, + }: { + icon: JSX.Element + label: string + count?: number + url?: string + bold?: boolean + onPress?: () => void + }) => ( + onNavigate(url || '/')}> + + {icon} + {count ? ( + + {count} + + ) : undefined} + + + {label} + + + ) + + /*TODO */ + return ( + + + onNavigate('/search')}> + } + size={21} + /> + Search + + + + } + label={store.me.displayName || store.me.handle} + bold + url={`/profile/${store.me.handle}`} + /> + } + size="24" + /> + } + label="Home" + url="/" + /> + } + size="24" + /> + } + label="Notifications" + url="/notifications" + count={store.me.notificationCount} + /> + } + size="24" + strokeWidth={2} + /> + } + label="Settings" + url="/settings" + count={store.me.notificationCount} + /> + + + Scenes + } + size="24" + /> + } + label="Create a scene" + onPress={onPressCreateScene} + /> + {store.me.memberships + ? store.me.memberships.memberships.map((membership, i) => ( + + } + label={membership.displayName || membership.handle} + url={`/profile/${membership.handle}`} + /> + )) + : undefined} + + + ) +} + +const styles = StyleSheet.create({ + view: { + flex: 1, + backgroundColor: colors.white, + }, + section: { + paddingHorizontal: 10, + paddingTop: 10, + paddingBottom: 10, + borderBottomWidth: 1, + borderBottomColor: colors.gray1, + }, + heading: { + fontSize: 16, + fontWeight: 'bold', + paddingVertical: 8, + paddingHorizontal: 4, + }, + + searchBtn: { + flexDirection: 'row', + backgroundColor: colors.gray1, + borderRadius: 8, + margin: 10, + marginBottom: 0, + paddingVertical: 10, + paddingHorizontal: 12, + }, + searchBtnLabel: { + marginLeft: 8, + fontSize: 18, + color: colors.gray6, + }, + + menuItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + paddingHorizontal: 2, + }, + menuItemIconWrapper: { + width: 30, + height: 30, + alignItems: 'center', + justifyContent: 'center', + marginRight: 10, + }, + menuItemLabel: { + fontSize: 17, + color: colors.gray7, + }, + menuItemLabelBold: { + fontWeight: 'bold', + }, + menuItemCount: { + position: 'absolute', + right: -6, + top: -2, + backgroundColor: colors.red3, + paddingHorizontal: 4, + paddingBottom: 1, + borderRadius: 6, + }, + menuItemCountLabel: { + fontSize: 12, + fontWeight: 'bold', + color: colors.white, + }, +}) diff --git a/src/view/shell/mobile/MainMenu.tsx b/src/view/shell/mobile/MainMenu.tsx deleted file mode 100644 index 8a72646129..0000000000 --- a/src/view/shell/mobile/MainMenu.tsx +++ /dev/null @@ -1,354 +0,0 @@ -import React, {useEffect} from 'react' -import {observer} from 'mobx-react-lite' -import { - StyleSheet, - SafeAreaView, - Text, - TouchableOpacity, - TouchableWithoutFeedback, - View, -} from 'react-native' -import Animated, { - useSharedValue, - useAnimatedStyle, - withTiming, - interpolate, -} from 'react-native-reanimated' -import {IconProp} from '@fortawesome/fontawesome-svg-core' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import _chunk from 'lodash.chunk' -import {HomeIcon, UserGroupIcon, BellIcon} from '../../lib/icons' -import {UserAvatar} from '../../com/util/UserAvatar' -import {useStores} from '../../../state' -import {CreateSceneModel} from '../../../state/models/shell-ui' -import {s, colors} from '../../lib/styles' - -export const MainMenu = observer( - ({ - active, - insetBottom, - onClose, - }: { - active: boolean - insetBottom: number - onClose: () => void - }) => { - const store = useStores() - const initInterp = useSharedValue(0) - - useEffect(() => { - if (active) { - // trigger a refresh in case memberships have changed recently - store.me.refreshMemberships() - } - }, [active]) - useEffect(() => { - if (active) { - initInterp.value = withTiming(1, {duration: 150}) - } else { - initInterp.value = 0 - } - }, [initInterp, active]) - const wrapperAnimStyle = useAnimatedStyle(() => ({ - opacity: interpolate(initInterp.value, [0, 1.0], [0, 1.0]), - })) - const menuItemsAnimStyle = useAnimatedStyle(() => ({ - top: interpolate(initInterp.value, [0, 1.0], [15, 0]), - })) - - // events - // = - - const onNavigate = (url: string) => { - store.nav.navigate(url) - onClose() - } - const onPressCreateScene = () => { - store.shell.openModal(new CreateSceneModel()) - onClose() - } - - // rendering - // = - - const MenuItemBlank = () => ( - - ) - - const MenuItem = ({ - icon, - label, - count, - url, - onPress, - }: { - icon: IconProp - label: string - count?: number - url?: string - onPress?: () => void - }) => ( - onNavigate(url || '/')}> - - {icon === 'home' ? ( - - ) : icon === 'user-group' ? ( - - ) : icon === 'bell' ? ( - - ) : ( - - )} - - {count ? ( - - {count} - - ) : undefined} - - {label} - - - ) - const MenuItemActor = ({ - label, - url, - count, - }: { - label: string - url: string - count?: number - }) => ( - onNavigate(url)}> - - - - {count ? ( - - {count} - - ) : undefined} - - {label} - - - ) - - if (!active) { - return - } - - const MenuItems = ({ - children, - }: { - children: (JSX.Element | JSX.Element[])[] - }) => { - const groups = _chunk(children.flat(), 4) - const lastGroup = groups.at(-1) - while (lastGroup && lastGroup.length < 4) { - lastGroup.push() - } - return ( - <> - {groups.map((group, i) => ( - - {group.map((el, j) => ( - {el} - ))} - - ))} - - ) - } - - /*TODO */ - return ( - <> - - - - - - - onNavigate(`/profile/${store.me.handle || ''}`)}> - - - - - {store.me.displayName || store.me.handle || 'My profile'} - - - - onNavigate(`/settings`)}> - - - - - - - - - - Scenes - - - {store.me.memberships ? ( - store.me.memberships.memberships.map((membership, i) => ( - - )) - ) : ( - - )} - - - - - - ) - }, -) - -const styles = StyleSheet.create({ - bg: { - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - // backgroundColor: '#000', - opacity: 0, - }, - wrapper: { - position: 'absolute', - top: 0, - width: '100%', - backgroundColor: '#fff', - }, - - topSection: { - flexDirection: 'row', - alignItems: 'center', - height: 40, - paddingHorizontal: 10, - marginTop: 12, - marginBottom: 20, - }, - section: { - paddingHorizontal: 10, - }, - heading: { - fontSize: 21, - fontWeight: 'bold', - paddingHorizontal: 10, - paddingTop: 6, - paddingBottom: 12, - }, - - profile: { - paddingVertical: 10, - paddingHorizontal: 10, - flexDirection: 'row', - alignItems: 'center', - }, - profileImage: { - marginRight: 8, - }, - profileText: { - fontSize: 17, - fontWeight: 'bold', - }, - - settings: {}, - settingsIcon: { - color: colors.gray5, - marginRight: 10, - }, - - menuItemsAnimContainer: { - position: 'relative', - }, - menuItems: { - flexDirection: 'row', - marginBottom: 20, - }, - menuItem: { - flex: 1, - alignItems: 'center', - }, - menuItemMargin: { - marginRight: 10, - }, - menuItemIconWrapper: { - borderRadius: 6, - width: 60, - height: 60, - justifyContent: 'center', - alignItems: 'center', - marginBottom: 5, - backgroundColor: colors.gray1, - }, - menuItemIcon: { - color: colors.gray5, - }, - menuItemLabel: { - fontSize: 13, - textAlign: 'center', - }, - menuItemCount: { - position: 'absolute', - left: 48, - top: 10, - backgroundColor: colors.red3, - paddingHorizontal: 4, - paddingBottom: 1, - borderRadius: 6, - }, - menuItemCountLabel: { - fontSize: 12, - fontWeight: 'bold', - color: colors.white, - }, -}) diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index e3e30decc7..6bb111877a 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -33,7 +33,6 @@ import {match, MatchResult} from '../../routes' import {Login} from '../../screens/Login' import {Onboard} from '../../screens/Onboard' import {Modal} from '../../com/modals/Modal' -import {MainMenu} from './MainMenu' import {TabsSelector} from './TabsSelector' import {Composer} from './Composer' import {s, colors} from '../../lib/styles' @@ -118,7 +117,6 @@ const Btn = ({ export const MobileShell: React.FC = observer(() => { const store = useStores() - const [isMainMenuActive, setMainMenuActive] = useState(false) const [isTabsSelectorActive, setTabsSelectorActive] = useState(false) const scrollElRef = useRef() const winDim = useWindowDimensions() @@ -134,16 +132,10 @@ export const MobileShell: React.FC = observer(() => { if (store.nav.tab.current.url === '/') { scrollElRef.current?.scrollToOffset({offset: 0}) } else { - if (store.nav.tab.canGoBack) { - // sanity check - store.nav.tab.goBackToZero() - } else { - store.nav.navigate('/') - } + store.nav.tab.resetTo('/') } } - const onPressMenu = () => setMainMenuActive(true) - const onPressNotifications = () => store.nav.navigate('/notifications') + const onPressNotifications = () => store.nav.tab.resetTo('/notifications') const onPressTabs = () => toggleTabsMenu(!isTabsSelectorActive) const doNewTab = (url: string) => () => store.nav.newTab(url) @@ -337,16 +329,7 @@ export const MobileShell: React.FC = observer(() => { onLongPress={TABS_ENABLED ? doNewTab('/notifications') : undefined} notificationCount={store.me.notificationCount} /> - - setMainMenuActive(false)} - />