diff --git a/packages/components/src/components/cards/EmptyCards.tsx b/packages/components/src/components/cards/EmptyCards.tsx index fbdcd4f4..8b6c3f2f 100644 --- a/packages/components/src/components/cards/EmptyCards.tsx +++ b/packages/components/src/components/cards/EmptyCards.tsx @@ -14,6 +14,10 @@ import { import { SpringAnimatedActivityIndicator } from '../animated/spring/SpringAnimatedActivityIndicator' import { SpringAnimatedText } from '../animated/spring/SpringAnimatedText' import { Button, defaultButtonSize } from '../common/Button' +import { fabSize } from '../common/FAB' +import { Spacer } from '../common/Spacer' +import { useAppLayout } from '../context/LayoutContext' +import { fabSpacing, shouldRenderFAB } from '../layout/FABRenderer' import { GenericMessageWithButtonView } from './GenericMessageWithButtonView' const clearMessages = [ @@ -65,6 +69,7 @@ export const EmptyCards = React.memo((props: EmptyCardsProps) => { refresh, } = props + const { sizename } = useAppLayout() const springAnimatedTheme = useCSSVariablesOrSpringAnimatedTheme() const setColumnClearedAtFilter = useReduxAction( actions.setColumnClearedAtFilter, @@ -180,6 +185,10 @@ export const EmptyCards = React.memo((props: EmptyCardsProps) => { ) : null} + + {shouldRenderFAB(sizename) && ( + + )} ) }) diff --git a/packages/components/src/components/cards/EventCards.tsx b/packages/components/src/components/cards/EventCards.tsx index e727f5c5..1fa33eee 100644 --- a/packages/components/src/components/cards/EventCards.tsx +++ b/packages/components/src/components/cards/EventCards.tsx @@ -17,8 +17,12 @@ import { bugsnag, ErrorBoundary } from '../../libs/bugsnag' import * as actions from '../../redux/actions' import { contentPadding } from '../../styles/variables' import { Button } from '../common/Button' +import { fabSize } from '../common/FAB' import { RefreshControl } from '../common/RefreshControl' +import { Spacer } from '../common/Spacer' import { useFocusedColumn } from '../context/ColumnFocusContext' +import { useAppLayout } from '../context/LayoutContext' +import { fabSpacing, shouldRenderFAB } from '../layout/FABRenderer' import { EmptyCards, EmptyCardsProps } from './EmptyCards' import { EventCard } from './EventCard' import { CardItemSeparator } from './partials/CardItemSeparator' @@ -167,6 +171,8 @@ export const EventCards = React.memo((props: EventCardsProps) => { ]) const renderFooter = useCallback(() => { + const { sizename } = useAppLayout() + return ( <> @@ -201,6 +207,10 @@ export const EventCards = React.memo((props: EventCardsProps) => { /> ) : null} + + {shouldRenderFAB(sizename) && ( + + )} ) }, [ diff --git a/packages/components/src/components/cards/NotificationCards.tsx b/packages/components/src/components/cards/NotificationCards.tsx index 3bc91e2a..c503dd31 100644 --- a/packages/components/src/components/cards/NotificationCards.tsx +++ b/packages/components/src/components/cards/NotificationCards.tsx @@ -17,8 +17,12 @@ import { bugsnag, ErrorBoundary } from '../../libs/bugsnag' import * as actions from '../../redux/actions' import { contentPadding } from '../../styles/variables' import { Button } from '../common/Button' +import { fabSize } from '../common/FAB' import { RefreshControl } from '../common/RefreshControl' +import { Spacer } from '../common/Spacer' import { useFocusedColumn } from '../context/ColumnFocusContext' +import { useAppLayout } from '../context/LayoutContext' +import { fabSpacing, shouldRenderFAB } from '../layout/FABRenderer' import { EmptyCards, EmptyCardsProps } from './EmptyCards' import { NotificationCard } from './NotificationCard' import { CardItemSeparator } from './partials/CardItemSeparator' @@ -170,6 +174,8 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => { ]) const renderFooter = useCallback(() => { + const { sizename } = useAppLayout() + return ( <> @@ -204,6 +210,10 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => { /> ) : null} + + {shouldRenderFAB(sizename) && ( + + )} ) }, [ diff --git a/packages/components/src/components/columns/ColumnSwitcher.tsx b/packages/components/src/components/columns/ColumnSwitcher.tsx new file mode 100644 index 00000000..acca95af --- /dev/null +++ b/packages/components/src/components/columns/ColumnSwitcher.tsx @@ -0,0 +1,139 @@ +import React from 'react' +import { StyleSheet } from 'react-native' + +import { useAppViewMode } from '../../hooks/use-app-view-mode' +import { useCSSVariablesOrSpringAnimatedTheme } from '../../hooks/use-css-variables-or-spring--animated-theme' +import { useReduxState } from '../../hooks/use-redux-state' +import { emitter } from '../../libs/emitter' +import * as selectors from '../../redux/selectors' +import { contentPadding } from '../../styles/variables' +import { SpringAnimatedIcon } from '../animated/spring/SpringAnimatedIcon' +import { SpringAnimatedTouchableOpacity } from '../animated/spring/SpringAnimatedTouchableOpacity' +import { SpringAnimatedView } from '../animated/spring/SpringAnimatedView' +import { fabSize } from '../common/FAB' +import { useFocusedColumn } from '../context/ColumnFocusContext' +import { useAppLayout } from '../context/LayoutContext' +import { fabSpacing, shouldRenderFAB } from '../layout/FABRenderer' + +const spacing = fabSpacing + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + bottom: spacing, + left: contentPadding, + flexDirection: 'row', + borderRadius: fabSize / 2, + shadowColor: '#000000', + shadowOffset: { + width: 0, + height: 3, + }, + shadowOpacity: 0.2, + shadowRadius: 6, + overflow: 'hidden', + zIndex: 1000, + }, +}) + +export function ColumnSwitcher() { + const columnIds = useReduxState(selectors.columnIdsSelector) + const currentOpenedModal = useReduxState(selectors.currentOpenedModal) + const focusedColumnId = useFocusedColumn() || columnIds[0] + const springAnimatedTheme = useCSSVariablesOrSpringAnimatedTheme() + const { appViewMode } = useAppViewMode() + const { sizename } = useAppLayout() + + if (!(appViewMode === 'single-column' && shouldRenderFAB(sizename))) + return null + if (currentOpenedModal) return null + + const isFirst = focusedColumnId === columnIds[0] + const isLast = focusedColumnId === columnIds.slice(-1)[0] + + return ( + + { + emitter.emit('FOCUS_ON_PREVIOUS_COLUMN', { + highlight: isFirst, + }) + }} + style={[ + { + alignItems: 'center', + alignContent: 'center', + justifyContent: 'center', + width: fabSize, + height: fabSize, + borderRadius: 0, + borderTopLeftRadius: fabSize / 2, + borderBottomLeftRadius: fabSize / 2, + backgroundColor: springAnimatedTheme.backgroundColor, + }, + isFirst && { opacity: 0.5 }, + ]} + > + + + { + emitter.emit('FOCUS_ON_NEXT_COLUMN', { + highlight: isLast, + }) + }} + style={[ + { + alignItems: 'center', + alignContent: 'center', + justifyContent: 'center', + width: fabSize, + height: fabSize, + borderRadius: 0, + borderTopRightRadius: fabSize / 2, + borderBottomRightRadius: fabSize / 2, + backgroundColor: springAnimatedTheme.backgroundColor, + }, + isLast && { opacity: 0.5 }, + ]} + > + + + + ) +} diff --git a/packages/components/src/components/common/FAB.tsx b/packages/components/src/components/common/FAB.tsx index 82758efe..e19a4635 100644 --- a/packages/components/src/components/common/FAB.tsx +++ b/packages/components/src/components/common/FAB.tsx @@ -13,7 +13,7 @@ import { } from '../animated/spring/SpringAnimatedTouchableOpacity' import { SpringAnimatedView } from '../animated/spring/SpringAnimatedView' -export const fabSize = 50 +export const fabSize = 44 export interface FABProps extends SpringAnimatedTouchableOpacityProps { children?: string | React.ReactElement @@ -98,11 +98,11 @@ export function FAB(props: FABProps) { name={iconName} style={[ { - width: 24, - height: 24, - lineHeight: 24, + width: fabSize / 2, + height: fabSize / 2, + lineHeight: fabSize / 2, marginTop: 1, - fontSize: 24, + fontSize: fabSize / 2, textAlign: 'center', color: useBrandColor ? springAnimatedTheme.primaryForegroundColor diff --git a/packages/components/src/components/context/ColumnFocusContext.tsx b/packages/components/src/components/context/ColumnFocusContext.tsx index 1c1e5a00..e4b84fd2 100644 --- a/packages/components/src/components/context/ColumnFocusContext.tsx +++ b/packages/components/src/components/context/ColumnFocusContext.tsx @@ -1,6 +1,9 @@ import React, { useContext, useState } from 'react' import { useEmitter } from '../../hooks/use-emitter' +import { emitter } from '../../libs/emitter' +import { useReduxStore } from '../../redux/context/ReduxStoreContext' +import * as selectors from '../../redux/selectors' export interface ColumnFocusProviderProps { children?: React.ReactNode @@ -13,6 +16,7 @@ export const ColumnFocusContext = React.createContext( ) export function ColumnFocusProvider(props: ColumnFocusProviderProps) { + const store = useReduxStore() const [columnId, setColumnId] = useState(null) useEmitter( @@ -23,6 +27,52 @@ export function ColumnFocusProvider(props: ColumnFocusProviderProps) { [], ) + useEmitter( + 'FOCUS_ON_PREVIOUS_COLUMN', + payload => { + const state = store.getState() + const columnIds = selectors.columnIdsSelector(state) + const focusedColumnIndex = columnIds + ? columnIds.findIndex(id => id === (columnId || columnIds[0])) + : -1 + + const previousColumnIndex = Math.max( + 0, + Math.min(focusedColumnIndex - 1, columnIds.length - 1), + ) + + emitter.emit('FOCUS_ON_COLUMN', { + ...payload, + columnId: columnIds[previousColumnIndex], + columnIndex: previousColumnIndex, + }) + }, + [columnId], + ) + + useEmitter( + 'FOCUS_ON_NEXT_COLUMN', + payload => { + const state = store.getState() + const columnIds = selectors.columnIdsSelector(state) + const focusedColumnIndex = columnIds + ? columnIds.findIndex(id => id === (columnId || columnIds[0])) + : -1 + + const nextColumnIndex = Math.max( + 0, + Math.min(focusedColumnIndex + 1, columnIds.length - 1), + ) + + emitter.emit('FOCUS_ON_COLUMN', { + ...payload, + columnId: columnIds[nextColumnIndex], + columnIndex: nextColumnIndex, + }) + }, + [columnId], + ) + useEmitter( 'SCROLL_DOWN_COLUMN', payload => { diff --git a/packages/components/src/components/layout/FABRenderer.tsx b/packages/components/src/components/layout/FABRenderer.tsx index c2b0e519..3d26a76f 100644 --- a/packages/components/src/components/layout/FABRenderer.tsx +++ b/packages/components/src/components/layout/FABRenderer.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { View, ViewStyle } from 'react-native' +import { StyleSheet, View } from 'react-native' import { useKeyboardVisibility } from '../../hooks/use-keyboard-visibility' import { useReduxAction } from '../../hooks/use-redux-action' @@ -7,18 +7,30 @@ import { useReduxState } from '../../hooks/use-redux-state' import * as actions from '../../redux/actions' import * as selectors from '../../redux/selectors' import { contentPadding } from '../../styles/variables' -import { defaultButtonSize } from '../common/Button' -import { FAB, fabSize } from '../common/FAB' -import { useAppLayout } from '../context/LayoutContext' +import { FAB } from '../common/FAB' +import { AppLayoutProviderState, useAppLayout } from '../context/LayoutContext' -export const fabSpacing = - contentPadding / 2 + Math.max(0, (fabSize - defaultButtonSize) / 2) - 2 +export const fabSpacing = contentPadding / 2 // + Math.max(0, (fabSize - defaultButtonSize) / 2) - 2 -const fabPositionStyle: ViewStyle = { - position: 'absolute', - bottom: fabSpacing, - right: contentPadding, - zIndex: 1000, +const styles = StyleSheet.create({ + container: { + position: 'absolute', + bottom: fabSpacing, + right: contentPadding, + zIndex: 1000, + }, +}) + +export function shouldRenderFAB( + sizename: AppLayoutProviderState['sizename'], + keyboardVisibility?: ReturnType, +) { + if (!(sizename <= '3-large')) return false + + if (keyboardVisibility === 'appearing' || keyboardVisibility === 'visible') + return false + + return true } export function FABRenderer() { @@ -32,9 +44,7 @@ export function FABRenderer() { const closeAllModals = useReduxAction(actions.closeAllModals) const replaceModal = useReduxAction(actions.replaceModal) - if (!(sizename < '3-large')) return null - if (keyboardVisibility === 'appearing' || keyboardVisibility === 'visible') - return null + if (!shouldRenderFAB(sizename, keyboardVisibility)) return null if (!currentOpenedModal) { /* @@ -57,7 +67,7 @@ export function FABRenderer() { const iconStyle = undefined return ( - + + { { <> {/* */} - {!!large && ( + {!!large && !shouldRenderFAB(sizename) && ( <> { const scrollLeft = useCallback(() => { if (currentOpenedModal) return - const previousColumnIndex = Math.max( - 0, - Math.min(focusedColumnIndex - 1, columnIds.length - 1), - ) - - emitter.emit('FOCUS_ON_COLUMN', { + emitter.emit('FOCUS_ON_PREVIOUS_COLUMN', { animated: true, - columnId: columnIds[previousColumnIndex], - columnIndex: previousColumnIndex, focusOnVisibleItem: true, highlight: false, scrollTo: true, }) - }, [currentOpenedModal, focusedColumnIndex, columnIds]) + }, [currentOpenedModal]) useKeyPressCallback('ArrowLeft', scrollLeft) useKeyPressCallback('h', scrollLeft) @@ -209,20 +203,13 @@ export const MainScreen = React.memo(() => { const scrollRight = useCallback(() => { if (currentOpenedModal) return - const nextColumnIndex = Math.max( - 0, - Math.min(focusedColumnIndex + 1, columnIds.length - 1), - ) - - emitter.emit('FOCUS_ON_COLUMN', { + emitter.emit('FOCUS_ON_NEXT_COLUMN', { animated: true, - columnId: columnIds[nextColumnIndex], - columnIndex: nextColumnIndex, focusOnVisibleItem: true, highlight: false, scrollTo: true, }) - }, [currentOpenedModal, focusedColumnIndex, columnIds]) + }, [currentOpenedModal]) useKeyPressCallback('ArrowRight', scrollRight) useKeyPressCallback('l', scrollRight) @@ -367,6 +354,7 @@ export const MainScreen = React.memo(() => { /> +