diff --git a/packages/components/src/components/cards/EventCard.tsx b/packages/components/src/components/cards/EventCard.tsx index 7159c5c03..6cfcb4e3b 100644 --- a/packages/components/src/components/cards/EventCard.tsx +++ b/packages/components/src/components/cards/EventCard.tsx @@ -47,6 +47,7 @@ import { WikiPageListRow } from './partials/rows/WikiPageListRow' export interface EventCardProps { event: EnhancedGitHubEvent repoIsKnown?: boolean + isFocused?: boolean } const styles = StyleSheet.create({ @@ -56,7 +57,7 @@ const styles = StyleSheet.create({ }) export const EventCard = React.memo((props: EventCardProps) => { - const { event, repoIsKnown } = props + const { event, repoIsKnown, isFocused } = props const springAnimatedTheme = useSpringAnimatedTheme() @@ -175,17 +176,16 @@ export const EventCard = React.memo((props: EventCardProps) => { const smallLeftColumn = false + const getBackgroundColor = () => { + if (isFocused) return springAnimatedTheme.backgroundColorDarker2 + if (isRead) return springAnimatedTheme.backgroundColorDarker1 + return springAnimatedTheme.backgroundColor + } + return ( { refresh, } = props + const flatListRef = React.useRef>(null) + + const focusedIndex = useKeyboardScrolling({ + ref: flatListRef, + columnId: column.id, + length: events.length, + }) + + const focusedColumn = useReduxState(focusedColumnSelector) + const setColumnClearedAtFilter = useReduxAction( actions.setColumnClearedAtFilter, ) @@ -73,16 +86,30 @@ export const EventCards = React.memo((props: EventCardsProps) => { return `event-card-${event.id}` } - function renderItem({ item: event }: { item: EnhancedGitHubEvent }) { + function renderItem({ + item: event, + index, + }: { + item: EnhancedGitHubEvent + index: number + }) { if (props.swipeable) { return ( - + ) } return ( - + ) } @@ -128,6 +155,7 @@ export const EventCards = React.memo((props: EventCardsProps) => { return ( { - const { notification, onlyOneRepository } = props + const { notification, onlyOneRepository, isFocused } = props const springAnimatedTheme = useSpringAnimatedTheme() const hasPrivateAccess = useReduxState( @@ -171,17 +172,16 @@ export const NotificationCard = React.memo((props: NotificationCardProps) => { const smallLeftColumn = false + const getBackgroundColor = () => { + if (isFocused) return springAnimatedTheme.backgroundColorDarker2 + if (isRead) return springAnimatedTheme.backgroundColorDarker1 + return springAnimatedTheme.backgroundColor + } + return ( { refresh, } = props + const flatListRef = React.useRef>(null) + + const focusedIndex = useKeyboardScrolling({ + ref: flatListRef, + columnId: column.id, + length: notifications.length, + }) + + const focusedColumn = useReduxState(focusedColumnSelector) + const setColumnClearedAtFilter = useReduxAction( actions.setColumnClearedAtFilter, ) @@ -79,14 +92,17 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => { function renderItem({ item: notification, + index, }: { item: EnhancedGitHubNotification + index: number }) { if (props.swipeable) { return ( ) } @@ -96,6 +112,7 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => { ) @@ -142,6 +159,7 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => { return ( > + columnId: string + length: number +} + +export function useKeyboardScrolling({ + ref, + columnId, + length, +}: KeyboardScrollingConfig) { + const [selectedCardIndex, setSelectedCardIndex] = React.useState(0) + useEmitter( + 'SCROLL_DOWN_COLUMN', + (payload: { columnId: string }) => { + if (!ref.current) return + if (columnId !== payload.columnId) return + const index = selectedCardIndex + 1 + if (index < length) { + ref.current.scrollToIndex({ + animated: true, + index, + }) + setSelectedCardIndex(index) + } + }, + [selectedCardIndex, length], + ) + useEmitter( + 'SCROLL_UP_COLUMN', + (payload: { columnId: string }) => { + if (!ref.current) return + if (columnId !== payload.columnId) return + const index = selectedCardIndex - 1 + if (index >= 0) { + ref.current.scrollToIndex({ + animated: true, + index, + }) + setSelectedCardIndex(index) + } + }, + [selectedCardIndex, length], + ) + return selectedCardIndex +} diff --git a/packages/components/src/redux/actions/columns.ts b/packages/components/src/redux/actions/columns.ts index f3d5ebb81..93577285a 100644 --- a/packages/components/src/redux/actions/columns.ts +++ b/packages/components/src/redux/actions/columns.ts @@ -8,6 +8,10 @@ import { } from '@devhub/core' import { createAction } from '../helpers' +export function focusColumn(payload: number) { + return createAction('FOCUS_COLUMN', payload) +} + export function replaceColumnsAndSubscriptions( payload: ColumnsAndSubscriptions, ) { diff --git a/packages/components/src/redux/reducers/columns.ts b/packages/components/src/redux/reducers/columns.ts index 25be621e6..8d0afaceb 100644 --- a/packages/components/src/redux/reducers/columns.ts +++ b/packages/components/src/redux/reducers/columns.ts @@ -14,12 +14,14 @@ export interface State { allIds: string[] byId: Record | null updatedAt: string | null + focused: number } const initialState: State = { allIds: [], byId: null, updatedAt: null, + focused: 0, } export const columnsReducer: Reducer = ( @@ -27,6 +29,10 @@ export const columnsReducer: Reducer = ( action, ) => { switch (action.type) { + case 'FOCUS_COLUMN': + return immer(state, draft => { + draft.focused = action.payload + }) case 'ADD_COLUMN_AND_SUBSCRIPTIONS': return immer(state, draft => { draft.allIds = draft.allIds || [] diff --git a/packages/components/src/redux/selectors/columns.ts b/packages/components/src/redux/selectors/columns.ts index 0a043cd57..8255191ff 100644 --- a/packages/components/src/redux/selectors/columns.ts +++ b/packages/components/src/redux/selectors/columns.ts @@ -5,6 +5,8 @@ import { createSubscriptionSelector } from './subscriptions' const s = (state: RootState) => state.columns || {} +export const focusedColumnSelector = (state: RootState) => s(state).focused + export const createColumnSelector = () => createSelector( (state: RootState) => s(state).byId, diff --git a/packages/components/src/screens/MainScreen.tsx b/packages/components/src/screens/MainScreen.tsx index 91a5d8f62..0d12b90ae 100644 --- a/packages/components/src/screens/MainScreen.tsx +++ b/packages/components/src/screens/MainScreen.tsx @@ -37,7 +37,9 @@ const styles = StyleSheet.create({ export const MainScreen = React.memo(() => { const currentOpenedModal = useReduxState(selectors.currentOpenedModal) const columnIds = useReduxState(selectors.columnIdsSelector) + const focusedColumn = useReduxState(selectors.focusedColumnSelector) const closeAllModals = useReduxAction(actions.closeAllModals) + const focusColumn = useReduxAction(actions.focusColumn) const popModal = useReduxAction(actions.popModal) const replaceModal = useReduxAction(actions.replaceModal) const syncDown = useReduxAction(actions.syncDown) @@ -105,6 +107,55 @@ export const MainScreen = React.memo(() => { return } + if (e.key === 'j') { + emitter.emit( + 'SCROLL_DOWN_COLUMN', + { + columnId: columnIds[focusedColumn], + }, + [focusedColumn], + ) + return + } + + if (e.key === 'l') { + const nextColumn = focusedColumn + 1 + if (nextColumn < columnIds.length) { + emitter.emit('FOCUS_ON_COLUMN', { + animated: true, + columnId: columnIds[nextColumn], + columnIndex: nextColumn, + highlight: true, + }) + focusColumn(nextColumn) + } + return + } + if (e.key === 'h') { + const previousColumn = focusedColumn - 1 + if (previousColumn >= 0) { + emitter.emit('FOCUS_ON_COLUMN', { + animated: true, + columnId: columnIds[previousColumn], + columnIndex: previousColumn, + highlight: true, + }) + focusColumn(previousColumn) + } + return + } + + if (e.key === 'k') { + emitter.emit( + 'SCROLL_UP_COLUMN', + { + columnId: columnIds[focusedColumn], + }, + [focusedColumn], + ) + return + } + if (columnIds.length > 0) { const n = e.key >= '0' && e.key <= '9' ? parseInt(e.key, 10) : -1 @@ -116,6 +167,7 @@ export const MainScreen = React.memo(() => { columnIndex, highlight: true, }) + focusColumn(columnIndex) return } @@ -127,12 +179,13 @@ export const MainScreen = React.memo(() => { columnIndex, highlight: true, }) + focusColumn(columnIndex) return } } }, undefined, - [columnIds], + [columnIds, focusedColumn], ) useEmitter(