diff --git a/packages/shared/package.json b/packages/shared/package.json index 374fa95b..93bca9d3 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -13,6 +13,7 @@ "bugsnag-js": "^4.7.3", "bugsnag-react": "^1.1.1", "bugsnag-react-native": "^2.12.2", + "fbemitter": "^2.1.1", "gravatar": "^1.6.0", "hoist-non-react-statics": "^3.1.0", "immer": "^1.7.4", @@ -38,6 +39,7 @@ "reselect": "^4.0.0" }, "devDependencies": { + "@types/fbemitter": "^2.0.32", "@types/gravatar": "^1.4.28", "@types/hoist-non-react-statics": "^3.0.1", "@types/jest": "^23.3.9", diff --git a/packages/shared/src/components/App.tsx b/packages/shared/src/components/App.tsx index 7e9a4a9c..581b61fb 100644 --- a/packages/shared/src/components/App.tsx +++ b/packages/shared/src/components/App.tsx @@ -2,7 +2,6 @@ import React, { PureComponent, StrictMode } from 'react' import { Provider as ReduxProvider } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' -import { bugsnagClient, initBugsnag } from '../libs/bugsnag' import { AppNavigator } from '../navigation/AppNavigator' import { configureStore } from '../redux/store' import { AppGlobalStyles } from './AppGlobalStyles' @@ -11,9 +10,6 @@ import { ThemeProvider } from './context/ThemeContext' const { persistor, store } = configureStore() -initBugsnag('231f337f6090422c611017d3dab3d32e') -bugsnagClient.config.notifyReleaseStages = ['production'] - // TODO: Enable StrictMode after react-redux fixes it // @see https://github.com/reduxjs/react-redux/issues/897 // @see https://github.com/reduxjs/react-redux/issues/890 diff --git a/packages/shared/src/components/columns/Column.tsx b/packages/shared/src/components/columns/Column.tsx index e5fadddd..fac14e01 100644 --- a/packages/shared/src/components/columns/Column.tsx +++ b/packages/shared/src/components/columns/Column.tsx @@ -1,7 +1,10 @@ import React, { PureComponent, ReactNode } from 'react' import { StyleProp, StyleSheet, View, ViewProps, ViewStyle } from 'react-native' +import { EventSubscription } from 'fbemitter' import { Platform } from '../../libs/platform' +import { emitter } from '../../setup' +import * as colors from '../../styles/colors' import { contentPadding } from '../../styles/variables' import { DimensionsConsumer } from '../context/DimensionsContext' import { ThemeConsumer } from '../context/ThemeContext' @@ -11,12 +14,17 @@ export const columnMargin = contentPadding / 2 export interface ColumnProps extends ViewProps { children?: ReactNode + columnId: string maxWidth?: number minWidth?: number pagingEnabled?: boolean style?: StyleProp } +export interface ColumnState { + showFocusBorder?: boolean +} + const styles = StyleSheet.create({ container: { height: '100%', @@ -38,9 +46,45 @@ export class Column extends PureComponent { minWidth: 320, } + state = { + showFocusBorder: false, + } + + focusOnColumnListener?: EventSubscription + + componentDidMount() { + this.focusOnColumnListener = emitter.addListener( + 'FOCUS_ON_COLUMN', + this.handleColumnFocusRequest, + ) + } + + componentWillUnmount() { + if (this.focusOnColumnListener) this.focusOnColumnListener.remove() + } + + handleColumnFocusRequest = ({ + columnId, + highlight, + }: { + columnId: string + highlight?: boolean + }) => { + if (!(columnId && columnId === this.props.columnId)) return + if (!highlight) return + + this.setState({ showFocusBorder: true }, () => { + setTimeout(() => { + this.setState({ showFocusBorder: false }) + }, 1000) + }) + } + render() { + const { showFocusBorder } = this.state const { children, + columnId, maxWidth, minWidth, pagingEnabled, @@ -49,7 +93,7 @@ export class Column extends PureComponent { } = this.props return ( - + {({ theme }) => ( {({ width }) => ( @@ -73,6 +117,18 @@ export class Column extends PureComponent { ]} > {children} + + {!!showFocusBorder && ( + + )} )} diff --git a/packages/shared/src/components/columns/ColumnHeaderItem.tsx b/packages/shared/src/components/columns/ColumnHeaderItem.tsx index 2ad055db..4a1956d3 100644 --- a/packages/shared/src/components/columns/ColumnHeaderItem.tsx +++ b/packages/shared/src/components/columns/ColumnHeaderItem.tsx @@ -26,11 +26,7 @@ import { ThemeConsumer } from '../context/ThemeContext' export const columnHeaderItemContentSize = 20 export interface ColumnHeaderItemProps { - avatarDetails?: { - owner: string - repo?: string - } - avatarShape?: AvatarProps['shape'] + avatarProps?: Partial avatarStyle?: StyleProp iconName?: GitHubIcon iconStyle?: StyleProp @@ -90,9 +86,7 @@ class ColumnHeaderItemComponent extends PureComponent< render() { const { - avatarDetails, - avatarShape, - avatarStyle, + avatarProps: _avatarProps, iconName, iconStyle, subtitle, @@ -102,12 +96,13 @@ class ColumnHeaderItemComponent extends PureComponent< username: _username, } = this.props + const avatarProps = _avatarProps || {} + const username = _username && - avatarDetails && - avatarDetails.owner && - !(_username.toLowerCase() === avatarDetails.owner.toLowerCase()) - ? avatarDetails.owner + avatarProps.username && + !(_username.toLowerCase() === avatarProps.username.toLowerCase()) + ? avatarProps.username : undefined const smallAvatarSpacing = 5 @@ -153,8 +148,7 @@ class ColumnHeaderItemComponent extends PureComponent< }} isBot={false} linkURL="" - repo={avatarDetails && avatarDetails.repo} - shape={avatarShape} + {...avatarProps} style={[ { position: 'absolute', @@ -163,7 +157,7 @@ class ColumnHeaderItemComponent extends PureComponent< width: 10, height: 10, }, - avatarStyle, + avatarProps.style, ]} username={username} /> @@ -174,14 +168,13 @@ class ColumnHeaderItemComponent extends PureComponent< diff --git a/packages/shared/src/components/columns/Columns.tsx b/packages/shared/src/components/columns/Columns.tsx index 035b484b..2d873e77 100644 --- a/packages/shared/src/components/columns/Columns.tsx +++ b/packages/shared/src/components/columns/Columns.tsx @@ -1,3 +1,4 @@ +import { EventSubscription } from 'fbemitter' import React, { PureComponent } from 'react' import { FlatList, @@ -7,6 +8,7 @@ import { ViewStyle, } from 'react-native' +import { emitter } from '../../setup' import { Column, Omit } from '../../types' import { DimensionsConsumer } from '../context/DimensionsContext' import { EventColumn } from './EventColumn' @@ -26,9 +28,42 @@ const styles = StyleSheet.create({ }) export class Columns extends PureComponent { + flatListRef = React.createRef>() + focusOnColumnListener?: EventSubscription pagingEnabled: boolean = true swipeable: boolean = false + componentDidMount() { + this.focusOnColumnListener = emitter.addListener( + 'FOCUS_ON_COLUMN', + this.handleColumnFocusRequest, + ) + } + + componentWillUnmount() { + if (this.focusOnColumnListener) this.focusOnColumnListener.remove() + } + + handleColumnFocusRequest = ({ + animated, + columnIndex, + }: { + columnId: string + columnIndex: number + animated?: boolean + highlight?: boolean + }) => { + if (!this.flatListRef.current) return + if (!(this.props.data && this.props.data!.length)) return + + if (columnIndex >= 0 && columnIndex < this.props.data.length) { + this.flatListRef.current.scrollToIndex({ + animated, + index: columnIndex, + }) + } + } + keyExtractor(column: Column) { return `column-container-${column.id}` } @@ -79,6 +114,7 @@ export class Columns extends PureComponent { return ( diff --git a/packages/shared/src/components/common/Avatar.tsx b/packages/shared/src/components/common/Avatar.tsx index 31a935be..65016ddc 100644 --- a/packages/shared/src/components/common/Avatar.tsx +++ b/packages/shared/src/components/common/Avatar.tsx @@ -10,11 +10,13 @@ import { import { fixURL } from '../../utils/helpers/github/url' import { getRepositoryURL, getUserURL } from '../cards/partials/rows/helpers' import { ThemeConsumer } from '../context/ThemeContext' +import { ConditionalWrap } from './ConditionalWrap' import { ImageWithLoading, ImageWithLoadingProps } from './ImageWithLoading' import { Link } from './Link' export interface AvatarProps extends Partial { avatarURL?: string + disableLink?: boolean email?: string hitSlop?: TouchableOpacityProps['hitSlop'] isBot?: boolean @@ -31,6 +33,7 @@ export const size = avatarSize export const Avatar: SFC = ({ avatarURL: _avatarURL, + disableLink, email, hitSlop, isBot: _isBot, @@ -57,20 +60,26 @@ export const Avatar: SFC = ({ if (!uri) return null + const linkUri = disableLink + ? undefined + : linkURL + ? fixURL(linkURL) + : username + ? repo + ? getRepositoryURL(username, repo) + : getUserURL(username, { isBot }) + : undefined + return ( {({ theme }) => ( - ( + + {children} + + )} > = ({ style, ]} /> - + )} ) diff --git a/packages/shared/src/components/layout/Sidebar.tsx b/packages/shared/src/components/layout/Sidebar.tsx index 4ff06e2e..cc3ece21 100644 --- a/packages/shared/src/components/layout/Sidebar.tsx +++ b/packages/shared/src/components/layout/Sidebar.tsx @@ -10,6 +10,7 @@ import { connect } from 'react-redux' import * as actions from '../../redux/actions' import * as selectors from '../../redux/selectors' +import { emitter } from '../../setup' import { ExtractPropsFromConnector } from '../../types' import { getColumnHeaderDetails } from '../../utils/helpers/github/events' import { columnHeaderHeight } from '../columns/ColumnHeader' @@ -40,6 +41,7 @@ const connectToStore = connect( return { columns: selectors.columnsSelector(state), + currentOpenedModal: selectors.currentOpenedModal(state), username: (user && user.login) || '', } }, @@ -81,8 +83,8 @@ class SidebarComponent extends PureComponent< replaceModal({ name: 'ADD_COLUMN' })} - style={[styles.centerContainer, squareStyle]} + style={[ + styles.centerContainer, + squareStyle, + { + borderBottomWidth: StyleSheet.hairlineWidth, + borderColor: theme.backgroundColorDarker08, + }, + ]} > @@ -125,15 +134,26 @@ class SidebarComponent extends PureComponent< ) return ( - { + emitter.emit('FOCUS_ON_COLUMN', { + animated: !small || !this.props.currentOpenedModal, + columnId: column.id, + columnIndex: index, + highlight: !small, + }) + }} > - + ) })} diff --git a/packages/shared/src/components/modals/AddColumnDetailsModal.tsx b/packages/shared/src/components/modals/AddColumnDetailsModal.tsx index 22a131b1..f07248cb 100644 --- a/packages/shared/src/components/modals/AddColumnDetailsModal.tsx +++ b/packages/shared/src/components/modals/AddColumnDetailsModal.tsx @@ -205,7 +205,11 @@ class AddColumnDetailsModalComponent extends PureComponent< const { icon, name, paramList } = this.props return ( - + + { render() { return ( - + -export let ErrorBoundary: React.ComponentType +export let ErrorBoundary: React.ComponentType = React.Fragment +let bugsnagClient: InstanceType export function initBugsnag(apiKey: string) { bugsnagClient = new Bugsnag(apiKey) diff --git a/packages/shared/src/libs/bugsnag/index.web.ts b/packages/shared/src/libs/bugsnag/index.web.ts index 18f45000..dae52ee5 100644 --- a/packages/shared/src/libs/bugsnag/index.web.ts +++ b/packages/shared/src/libs/bugsnag/index.web.ts @@ -4,9 +4,9 @@ import React from 'react' export * from 'bugsnag-js' -export let bugsnagClient: ReturnType -export let ErrorBoundary: React.ComponentType +export let ErrorBoundary: React.ComponentType = React.Fragment +let bugsnagClient: ReturnType export function initBugsnag(apiKey: string) { bugsnagClient = bugsnag(apiKey) ;(bugsnagClient as any).apiKey = apiKey diff --git a/packages/shared/src/redux/sagas/columns.ts b/packages/shared/src/redux/sagas/columns.ts index 6c06b5a7..afd32cce 100644 --- a/packages/shared/src/redux/sagas/columns.ts +++ b/packages/shared/src/redux/sagas/columns.ts @@ -1,5 +1,7 @@ import { all, put, select, takeLatest } from 'redux-saga/effects' +import { delay } from 'redux-saga' +import { emitter } from '../../setup' import { Column, ExtractActionFromActionCreator } from '../../types' import { guid } from '../../utils/helpers/shared' import * as actions from '../actions' @@ -41,6 +43,38 @@ function* onLoginSuccess( if (!columns) yield put(actions.replaceColumns(getDefaultColumns(username))) } -export function* columnsSagas() { - yield all([yield takeLatest('LOGIN_SUCCESS', onLoginSuccess)]) +function* onAddColumn( + action: ExtractActionFromActionCreator, +) { + const columnId = action.payload.id + + const columns: Column[] | undefined = yield select(selectors.columnsSelector) + const columnIndex = columns && columns.findIndex(c => c.id === columnId) + + yield delay(300) + emitter.emit('FOCUS_ON_COLUMN', { + animated: true, + columnId, + columnIndex, + highlight: true, + }) +} + +function onMoveColumn( + action: ExtractActionFromActionCreator, +) { + emitter.emit('FOCUS_ON_COLUMN', { + animated: true, + columnId: action.payload.id, + columnIndex: action.payload.index, + highlight: true, + }) +} + +export function* columnsSagas() { + yield all([ + yield takeLatest('LOGIN_SUCCESS', onLoginSuccess), + yield takeLatest('ADD_COLUMN', onAddColumn), + yield takeLatest('MOVE_COLUMN', onMoveColumn), + ]) } diff --git a/packages/shared/src/screens/MainScreen.tsx b/packages/shared/src/screens/MainScreen.tsx index 6614c00e..81590278 100644 --- a/packages/shared/src/screens/MainScreen.tsx +++ b/packages/shared/src/screens/MainScreen.tsx @@ -1,6 +1,7 @@ +import { EventSubscription } from 'fbemitter' import hoistNonReactStatics from 'hoist-non-react-statics' import React, { PureComponent } from 'react' -import { StyleSheet, View } from 'react-native' +import { Dimensions, StyleSheet, View } from 'react-native' import { NavigationScreenProps, NavigationStackScreenOptions, @@ -16,6 +17,7 @@ import { ModalRenderer } from '../components/modals/ModalRenderer' import { ColumnsContainer } from '../containers/ColumnsContainer' import * as actions from '../redux/actions' import * as selectors from '../redux/selectors' +import { emitter } from '../setup' import { contentPadding } from '../styles/variables' import { ExtractPropsFromConnector } from '../types' @@ -34,6 +36,7 @@ const connectToStore = connect( currentOpenedModal: selectors.currentOpenedModal(state), }), { + closeAllModals: actions.closeAllModals, replaceModal: actions.replaceModal, }, ) @@ -45,6 +48,28 @@ class MainScreenComponent extends PureComponent< header: null, } + focusOnColumnListener?: EventSubscription + + componentDidMount() { + this.focusOnColumnListener = emitter.addListener( + 'FOCUS_ON_COLUMN', + this.handleColumnFocusRequest, + ) + } + + componentWillUnmount() { + if (this.focusOnColumnListener) this.focusOnColumnListener.remove() + } + + handleColumnFocusRequest = () => { + if ( + this.props.currentOpenedModal && + Dimensions.get('window').width <= 420 + ) { + this.props.closeAllModals() + } + } + render() { const { currentOpenedModal, replaceModal } = this.props diff --git a/packages/shared/src/setup.ts b/packages/shared/src/setup.ts new file mode 100644 index 00000000..b807d0b0 --- /dev/null +++ b/packages/shared/src/setup.ts @@ -0,0 +1,8 @@ +import { EventEmitter } from 'fbemitter' + +import { initBugsnag } from './libs/bugsnag' + +export const emitter = new EventEmitter() + +export const bugsnagClient = initBugsnag('231f337f6090422c611017d3dab3d32e') +bugsnagClient.config.notifyReleaseStages = ['production'] diff --git a/packages/shared/src/utils/helpers/github/events.ts b/packages/shared/src/utils/helpers/github/events.ts index 8c69c493..e7279b36 100644 --- a/packages/shared/src/utils/helpers/github/events.ts +++ b/packages/shared/src/utils/helpers/github/events.ts @@ -22,9 +22,9 @@ import { export function getColumnHeaderDetails( column: Column, ): { - avatarDetails?: { - owner: string + avatarProps?: { repo?: string + username: string } icon: GitHubIcon repoIsKnown: boolean @@ -36,7 +36,7 @@ export function getColumnHeaderDetails( switch (column.subtype) { case 'ORG_PUBLIC_EVENTS': { return { - avatarDetails: { owner: column.params.org }, + avatarProps: { username: column.params.org }, icon: 'organization', repoIsKnown: false, subtitle: 'Activity', @@ -53,9 +53,9 @@ export function getColumnHeaderDetails( } case 'REPO_EVENTS': { return { - avatarDetails: { - owner: column.params.owner, + avatarProps: { repo: column.params.repo, + username: column.params.owner, }, icon: 'repo', repoIsKnown: true, @@ -65,9 +65,9 @@ export function getColumnHeaderDetails( } case 'REPO_NETWORK_EVENTS': { return { - avatarDetails: { - owner: column.params.owner, + avatarProps: { repo: column.params.repo, + username: column.params.owner, }, icon: 'repo', repoIsKnown: true, @@ -77,7 +77,7 @@ export function getColumnHeaderDetails( } case 'USER_EVENTS': { return { - avatarDetails: { owner: column.params.username }, + avatarProps: { username: column.params.username }, icon: 'person', repoIsKnown: false, subtitle: 'Activity', @@ -86,7 +86,7 @@ export function getColumnHeaderDetails( } case 'USER_ORG_EVENTS': { return { - avatarDetails: { owner: column.params.org }, + avatarProps: { username: column.params.org }, icon: 'organization', repoIsKnown: false, subtitle: 'Activity', @@ -95,7 +95,7 @@ export function getColumnHeaderDetails( } case 'USER_PUBLIC_EVENTS': { return { - avatarDetails: { owner: column.params.username }, + avatarProps: { username: column.params.username }, icon: 'person', repoIsKnown: false, subtitle: 'Activity', @@ -105,7 +105,7 @@ export function getColumnHeaderDetails( case 'USER_RECEIVED_EVENTS': case 'USER_RECEIVED_PUBLIC_EVENTS': { return { - avatarDetails: { owner: column.params.username }, + avatarProps: { username: column.params.username }, icon: 'home', repoIsKnown: false, subtitle: 'Dashboard', diff --git a/yarn.lock b/yarn.lock index a839c819..9cd87fa4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -969,6 +969,11 @@ "@svgr/core" "^2.4.1" loader-utils "^1.1.0" +"@types/fbemitter@^2.0.32": + version "2.0.32" + resolved "http://registry.npmjs.org/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c" + integrity sha1-jtIE2g9U6cjq7DGx7skeJRMtCCw= + "@types/gravatar@^1.4.28": version "1.4.28" resolved "http://registry.npmjs.org/@types/gravatar/-/gravatar-1.4.28.tgz#a83527c354b3bce265ef489facfbed34971f9b16" @@ -4764,6 +4769,13 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +fbemitter@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/fbemitter/-/fbemitter-2.1.1.tgz#523e14fdaf5248805bb02f62efc33be703f51865" + integrity sha1-Uj4U/a9SSIBbsC9i78M75wP1GGU= + dependencies: + fbjs "^0.8.4" + fbjs-css-vars@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.1.tgz#836d876e887d702f45610f5ebd2fbeef649527fc" @@ -4785,7 +4797,7 @@ fbjs-scripts@^0.8.1: semver "^5.1.0" through2 "^2.0.0" -fbjs@^0.8.0, fbjs@^0.8.16, fbjs@^0.8.9: +fbjs@^0.8.0, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.17" resolved "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=