diff --git a/packages/shared/src/components/columns/Columns.tsx b/packages/shared/src/components/columns/Columns.tsx index 046bd93a..66b63650 100644 --- a/packages/shared/src/components/columns/Columns.tsx +++ b/packages/shared/src/components/columns/Columns.tsx @@ -8,16 +8,16 @@ import { ViewStyle, } from 'react-native' +import { ColumnContainer } from '../../containers/ColumnContainer' import { emitter } from '../../setup' -import { Column, Omit } from '../../types' +import { Omit } from '../../types' import { Separator } from '../common/Separator' import { DimensionsConsumer } from '../context/DimensionsContext' -import { EventColumn } from './EventColumn' -import { NotificationColumn } from './NotificationColumn' export interface ColumnsProps - extends Omit, 'renderItem'> { + extends Omit, 'data' | 'renderItem'> { contentContainerStyle?: StyleProp + columnIds: string[] style?: StyleProp } @@ -32,7 +32,7 @@ const styles = StyleSheet.create({ }) export class Columns extends PureComponent { - flatListRef = React.createRef>() + flatListRef = React.createRef>() focusOnColumnListener?: EventSubscription pagingEnabled: boolean = true swipeable: boolean = false @@ -58,9 +58,9 @@ export class Columns extends PureComponent { highlight?: boolean }) => { if (!this.flatListRef.current) return - if (!(this.props.data && this.props.data!.length)) return + if (!(this.props.columnIds && this.props.columnIds.length)) return - if (columnIndex >= 0 && columnIndex < this.props.data.length) { + if (columnIndex >= 0 && columnIndex < this.props.columnIds.length) { this.flatListRef.current.scrollToIndex({ animated, index: columnIndex, @@ -68,48 +68,22 @@ export class Columns extends PureComponent { } } - keyExtractor(column: Column) { - return `column-container-${column.id}` + keyExtractor(columnId: string) { + return `column-container-${columnId}` } - renderItem: FlatListProps['renderItem'] = ({ - item: column, - index, - }) => { - switch (column.type) { - case 'notifications': { - return ( - - ) - } - - case 'activity': { - return ( - - ) - } - - default: { - console.error('Invalid Column type: ', (column as any).type) - return null - } - } + renderItem: FlatListProps['renderItem'] = ({ item: columnId }) => { + return ( + + ) } render() { - const { data, style, ...props } = this.props + const { columnIds, style, ...props } = this.props return ( @@ -126,7 +100,7 @@ export class Columns extends PureComponent { ListHeaderComponent={small ? Separator : undefined} bounces={!this.swipeable} className="snap-container" - data={data} + data={columnIds} horizontal keyExtractor={this.keyExtractor} onScrollToIndexFailed={() => undefined} diff --git a/packages/shared/src/components/layout/Sidebar.tsx b/packages/shared/src/components/layout/Sidebar.tsx index 62bd6bf4..e750b87d 100644 --- a/packages/shared/src/components/layout/Sidebar.tsx +++ b/packages/shared/src/components/layout/Sidebar.tsx @@ -43,7 +43,7 @@ const connectToStore = connect( const user = selectors.currentUserSelector(state) return { - columns: selectors.columnsSelector(state), + columns: selectors.columnsArrSelector(state), currentOpenedModal: selectors.currentOpenedModal(state), username: (user && user.login) || '', } diff --git a/packages/shared/src/containers/ColumnContainer.tsx b/packages/shared/src/containers/ColumnContainer.tsx new file mode 100644 index 00000000..0792a37a --- /dev/null +++ b/packages/shared/src/containers/ColumnContainer.tsx @@ -0,0 +1,76 @@ +import _ from 'lodash' +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' + +import { EventColumn } from '../components/columns/EventColumn' +import { NotificationColumn } from '../components/columns/NotificationColumn' +import * as selectors from '../redux/selectors' +import { ExtractPropsFromConnector } from '../types' + +export interface ColumnContainerProps { + columnId: string + pagingEnabled?: boolean + swipeable?: boolean +} + +export interface ColumnContainerState {} + +const connectToStore = connect(() => { + const columnSelector = selectors.createColumnSelector() + + return (state: any, { columnId }: ColumnContainerProps) => ({ + column: columnSelector(state, columnId), + columnIndex: selectors.columnIdsSelector(state).indexOf(columnId), + }) +}) + +class ColumnContainerComponent extends PureComponent< + ColumnContainerProps & ExtractPropsFromConnector, + ColumnContainerState +> { + render() { + const { + columnIndex, + column, + pagingEnabled, + swipeable, + ...props + } = this.props + delete props.columnId + + if (!column) return null + + switch (column.type) { + case 'notifications': { + return ( + + ) + } + + case 'activity': { + return ( + + ) + } + + default: { + console.error('Invalid Column type: ', (column as any).type) + return null + } + } + } +} + +export const ColumnContainer = connectToStore(ColumnContainerComponent) diff --git a/packages/shared/src/containers/ColumnsContainer.tsx b/packages/shared/src/containers/ColumnsContainer.tsx index 7b025110..47719ffc 100644 --- a/packages/shared/src/containers/ColumnsContainer.tsx +++ b/packages/shared/src/containers/ColumnsContainer.tsx @@ -11,7 +11,7 @@ export interface ColumnsContainerProps {} export interface ColumnsContainerState {} const connectToStore = connect((state: any) => ({ - columns: selectors.columnsSelector(state), + columnIds: selectors.columnIdsSelector(state), })) class ColumnsContainerComponent extends PureComponent< @@ -19,8 +19,8 @@ class ColumnsContainerComponent extends PureComponent< ColumnsContainerState > { render() { - const columns = this.props.columns || [] - return + const columnIds = this.props.columnIds || [] + return } } diff --git a/packages/shared/src/redux/reducers/columns.ts b/packages/shared/src/redux/reducers/columns.ts index a178a133..531b3eab 100644 --- a/packages/shared/src/redux/reducers/columns.ts +++ b/packages/shared/src/redux/reducers/columns.ts @@ -1,63 +1,67 @@ import immer from 'immer' -import { REHYDRATE } from 'redux-persist' import { Column, Reducer } from '../../types' -import { columnsSelector } from '../selectors' export interface State { - columns?: Column[] + allIds: string[] + byId: Record | null } -const initialState: State = {} +const initialState: State = { + allIds: [], + byId: null, +} export const columnsReducer: Reducer = ( state = initialState, action, ) => { switch (action.type) { - case REHYDRATE as any: - return immer(state, draft => { - const columns = - columnsSelector((action.payload as any) || {}) || draft.columns - - if (columns) draft.columns = columns.filter(c => c && c.id) - }) case 'ADD_COLUMN': return immer(state, draft => { - draft.columns = draft.columns || [] - draft.columns = [action.payload, ...draft.columns] + draft.allIds = draft.allIds || [] + draft.byId = draft.byId || {} + + draft.allIds.unshift(action.payload.id) + draft.byId[action.payload.id] = action.payload }) case 'DELETE_COLUMN': return immer(state, draft => { - if (!draft.columns) return - draft.columns = draft.columns.filter(c => c.id !== action.payload) + if (draft.allIds) + draft.allIds = draft.allIds.filter(id => id !== action.payload) + + if (draft.byId) delete draft.byId[action.payload] }) case 'MOVE_COLUMN': return immer(state, draft => { - if (!draft.columns) return + if (!draft.allIds) return - const currentIndex = draft.columns.findIndex( - c => c.id === action.payload.id, + const currentIndex = draft.allIds.findIndex( + id => id === action.payload.id, ) - if (!(currentIndex >= 0 && currentIndex < draft.columns.length)) return + if (!(currentIndex >= 0 && currentIndex < draft.allIds.length)) return const newIndex = Math.max( 0, - Math.min(action.payload.index, draft.columns.length - 1), + Math.min(action.payload.index, draft.allIds.length - 1), ) if (Number.isNaN(newIndex)) return // move column inside array - const column = draft.columns[currentIndex] - draft.columns = draft.columns.filter(c => c !== column) - draft.columns.splice(newIndex, 0, column) + const columnId = draft.allIds[currentIndex] + draft.allIds = draft.allIds.filter(id => id !== columnId) + draft.allIds.splice(newIndex, 0, columnId) }) case 'REPLACE_COLUMNS': return immer(state, draft => { - draft.columns = action.payload + draft.byId = {} + draft.allIds = action.payload.map(c => { + draft.byId![c.id] = c + return c.id + }) }) default: diff --git a/packages/shared/src/redux/sagas/columns.ts b/packages/shared/src/redux/sagas/columns.ts index 662893e9..ad336459 100644 --- a/packages/shared/src/redux/sagas/columns.ts +++ b/packages/shared/src/redux/sagas/columns.ts @@ -39,8 +39,9 @@ function* onLoginSuccess( action: ExtractActionFromActionCreator, ) { const username = action.payload.login - const columns = yield select(selectors.columnsSelector) - if (!columns) yield put(actions.replaceColumns(getDefaultColumns(username))) + const hasCreatedColumn = yield select(selectors.hasCreatedColumnSelector) + if (!hasCreatedColumn) + yield put(actions.replaceColumns(getDefaultColumns(username))) } function* onAddColumn( @@ -48,8 +49,8 @@ function* onAddColumn( ) { const columnId = action.payload.id - const columns: Column[] | undefined = yield select(selectors.columnsSelector) - const columnIndex = columns && columns.findIndex(c => c.id === columnId) + const ids: string[] = yield select(selectors.columnIdsSelector) + const columnIndex = ids.findIndex(id => id === columnId) yield delay(300) emitter.emit('FOCUS_ON_COLUMN', { @@ -63,12 +64,12 @@ function* onAddColumn( function* onMoveColumn( action: ExtractActionFromActionCreator, ) { - const columns: Column[] | undefined = yield select(selectors.columnsSelector) - if (!columns) return + const ids: string[] = yield select(selectors.columnIdsSelector) + if (!(ids && ids.length)) return const columnIndex = Math.max( 0, - Math.min(action.payload.index, columns.length - 1), + Math.min(action.payload.index, ids.length - 1), ) if (Number.isNaN(columnIndex)) return diff --git a/packages/shared/src/redux/selectors/columns.ts b/packages/shared/src/redux/selectors/columns.ts index 7cc451f2..1044c9e9 100644 --- a/packages/shared/src/redux/selectors/columns.ts +++ b/packages/shared/src/redux/selectors/columns.ts @@ -1,5 +1,22 @@ +import { createSelector } from 'reselect' import { RootState } from '../../types' const s = (state: RootState) => state.columns || {} -export const columnsSelector = (state: RootState) => s(state).columns +export const createColumnSelector = () => + createSelector( + (state: RootState) => s(state).byId, + (_state: RootState, id: string) => id, + (byId, id) => byId && byId[id], + ) + +export const columnIdsSelector = (state: RootState) => s(state).allIds + +export const columnsArrSelector = createSelector( + (state: RootState) => columnIdsSelector(state), + (state: RootState) => s(state).byId, + (allIds, byId) => (byId ? allIds.map(id => byId[id]) : []), +) + +export const hasCreatedColumnSelector = (state: RootState) => + s(state).byId !== null diff --git a/packages/shared/src/redux/store.ts b/packages/shared/src/redux/store.ts index 8aab569f..25265d39 100644 --- a/packages/shared/src/redux/store.ts +++ b/packages/shared/src/redux/store.ts @@ -1,18 +1,42 @@ +import immer from 'immer' import { applyMiddleware, createStore } from 'redux' import { composeWithDevTools } from 'redux-devtools-extension' -import { PersistConfig, persistReducer, persistStore } from 'redux-persist' +import { + createMigrate, + PersistConfig, + persistReducer, + persistStore, +} from 'redux-persist' import storage from 'redux-persist/lib/storage' import createSagaMiddleware from 'redux-saga' +import { Column, RootState } from '../types' import { rootReducer } from './reducers' import { rootSaga } from './sagas' +const migrations = { + 0: (state: any) => state, + 1: (state: any) => state, + 2: (state: any) => + immer(state, draft => { + const columns: Column[] = draft.columns && draft.columns.columns + if (!columns) return + + draft.columns.byId = {} + draft.columns.allIds = columns.map(column => { + draft.columns.byId![column.id] = column + return column.id + }) + }), +} + export function configureStore(key = 'root') { const persistConfig: PersistConfig = { blacklist: ['navigation'], key, + migrate: createMigrate(migrations, { debug: __DEV__ }), storage, - version: 1, + version: 2, } const persistedReducer = persistReducer(persistConfig, rootReducer) diff --git a/packages/shared/src/screens/MainScreen.tsx b/packages/shared/src/screens/MainScreen.tsx index abd43691..bf191b12 100644 --- a/packages/shared/src/screens/MainScreen.tsx +++ b/packages/shared/src/screens/MainScreen.tsx @@ -35,7 +35,7 @@ const styles = StyleSheet.create({ const connectToStore = connect( (state: any) => ({ currentOpenedModal: selectors.currentOpenedModal(state), - columns: (selectors.columnsSelector(state) || []) as Column[], + columnIds: selectors.columnIdsSelector(state), }), { closeAllModals: actions.closeAllModals, @@ -103,27 +103,26 @@ class MainScreenComponent extends PureComponent< return } - if (this.props.columns.length > 0) { + if (this.props.columnIds.length > 0) { if (e.keyCode - 48 === 0) { - const columnIndex = this.props.columns.length - 1 + const columnIndex = this.props.columnIds.length - 1 emitter.emit('FOCUS_ON_COLUMN', { animated: true, - columnId: - this.props.columns[columnIndex] && - this.props.columns[columnIndex].id, + columnId: this.props.columnIds[columnIndex], columnIndex, highlight: true, }) return } - if (e.keyCode - 48 >= 1 && e.keyCode - 48 <= this.props.columns.length) { + if ( + e.keyCode - 48 >= 1 && + e.keyCode - 48 <= this.props.columnIds.length + ) { const columnIndex = e.keyCode - 48 - 1 emitter.emit('FOCUS_ON_COLUMN', { animated: true, - columnId: - this.props.columns[columnIndex] && - this.props.columns[columnIndex].id, + columnId: this.props.columnIds[columnIndex], columnIndex, highlight: true, }) diff --git a/packages/shared/src/types/redux.ts b/packages/shared/src/types/redux.ts index e84e959c..27ae378d 100644 --- a/packages/shared/src/types/redux.ts +++ b/packages/shared/src/types/redux.ts @@ -33,7 +33,7 @@ export type ExtractActionFromActionCreator = AC extends () => infer A export type ExtractPropsFromConnector< Connector -> = Connector extends InferableComponentEnhancerWithProps +> = Connector extends InferableComponentEnhancerWithProps ? T : never