From bdad2df7a7bdbfe8eb2aebad107a70a77bd41ad2 Mon Sep 17 00:00:00 2001 From: Bruno Lemos Date: Wed, 13 Feb 2019 06:20:37 -0200 Subject: [PATCH] New feature: Private repository support --- .../src/components/cards/EmptyCards.tsx | 4 +- .../src/components/cards/EventCards.tsx | 6 +- .../src/components/cards/NotificationCard.tsx | 55 +++++--- .../components/cards/NotificationCards.tsx | 4 +- .../partials/rows/PrivateNotificationRow.tsx | 67 +++++++++ .../src/components/columns/ColumnOptions.tsx | 25 +--- .../columns/EventOrNotificationColumn.tsx | 20 ++- .../src/components/modals/AddColumnModal.tsx | 36 ----- .../src/containers/EventCardsContainer.tsx | 95 ++++++------- .../containers/NotificationCardsContainer.tsx | 10 +- .../components/src/hooks/use-github-api.ts | 51 +++++++ packages/components/src/libs/github/index.ts | 40 +++--- .../components/src/redux/actions/index.ts | 1 + .../src/redux/actions/installations.ts | 19 +++ .../src/redux/actions/subscriptions.ts | 6 +- packages/components/src/redux/reducers/api.ts | 4 +- .../components/src/redux/reducers/index.ts | 2 + .../src/redux/reducers/installations.ts | 127 +++++++++++++++--- packages/components/src/redux/sagas/auth.ts | 4 +- packages/components/src/redux/sagas/index.ts | 2 + .../src/redux/sagas/installations.ts | 83 ++++++++++++ .../src/redux/sagas/subscriptions.ts | 48 +++++-- .../components/src/redux/selectors/api.ts | 2 +- .../components/src/redux/selectors/auth.ts | 4 - .../components/src/redux/selectors/index.ts | 1 + .../src/redux/selectors/installations.ts | 89 ++++++++++++ .../src/redux/selectors/subscriptions.ts | 11 +- .../components/src/utils/helpers/filters.ts | 37 +---- .../src/utils/helpers/github/shared.ts | 5 + packages/core/src/api/installations.ts | 78 +++++++---- .../core/src/helpers/github/notifications.ts | 25 ++-- packages/core/src/helpers/github/shared.ts | 6 +- packages/core/src/index.ts | 1 + packages/core/src/types/devhub.ts | 12 +- packages/core/src/types/github.ts | 12 +- packages/core/src/types/graphql.ts | 104 +++++++------- packages/core/src/utils/constants.ts | 2 +- 37 files changed, 764 insertions(+), 334 deletions(-) create mode 100644 packages/components/src/components/cards/partials/rows/PrivateNotificationRow.tsx create mode 100644 packages/components/src/hooks/use-github-api.ts create mode 100644 packages/components/src/redux/actions/installations.ts create mode 100644 packages/components/src/redux/sagas/installations.ts create mode 100644 packages/components/src/redux/selectors/installations.ts diff --git a/packages/components/src/components/cards/EmptyCards.tsx b/packages/components/src/components/cards/EmptyCards.tsx index ba58c742..d9941a81 100644 --- a/packages/components/src/components/cards/EmptyCards.tsx +++ b/packages/components/src/components/cards/EmptyCards.tsx @@ -1,7 +1,7 @@ import React from 'react' import { Image, Text, TextStyle, View, ViewStyle } from 'react-native' -import { LoadState } from '@devhub/core' +import { EnhancedLoadState } from '@devhub/core' import { useCSSVariablesOrSpringAnimatedTheme } from '../../hooks/use-css-variables-or-spring--animated-theme' import { useReduxAction } from '../../hooks/use-redux-action' import * as actions from '../../redux/actions' @@ -48,7 +48,7 @@ export interface EmptyCardsProps { errorMessage?: string errorTitle?: string fetchNextPage: (() => void) | undefined - loadState: LoadState + loadState: EnhancedLoadState refresh: (() => void | Promise) | undefined } diff --git a/packages/components/src/components/cards/EventCards.tsx b/packages/components/src/components/cards/EventCards.tsx index 1465d8b6..9b7a8692 100644 --- a/packages/components/src/components/cards/EventCards.tsx +++ b/packages/components/src/components/cards/EventCards.tsx @@ -5,16 +5,14 @@ import { Column, constants, EnhancedGitHubEvent, + EnhancedLoadState, isItemRead, - LoadState, } from '@devhub/core' import useKeyPressCallback from '../../hooks/use-key-press-callback' import { useKeyboardScrolling } from '../../hooks/use-keyboard-scrolling' import { useReduxAction } from '../../hooks/use-redux-action' -import { useReduxState } from '../../hooks/use-redux-state' import { bugsnag, ErrorBoundary } from '../../libs/bugsnag' import * as actions from '../../redux/actions' -import * as selectors from '../../redux/selectors' import { contentPadding } from '../../styles/variables' import { Button } from '../common/Button' import { FlatListWithOverlay } from '../common/FlatListWithOverlay' @@ -30,7 +28,7 @@ export interface EventCardsProps { errorMessage: EmptyCardsProps['errorMessage'] events: EnhancedGitHubEvent[] fetchNextPage: (() => void) | undefined - loadState: LoadState + loadState: EnhancedLoadState refresh: EmptyCardsProps['refresh'] repoIsKnown?: boolean swipeable?: boolean diff --git a/packages/components/src/components/cards/NotificationCard.tsx b/packages/components/src/components/cards/NotificationCard.tsx index 8e002f19..9de4554a 100644 --- a/packages/components/src/components/cards/NotificationCard.tsx +++ b/packages/components/src/components/cards/NotificationCard.tsx @@ -16,6 +16,7 @@ import { } from '@devhub/core' import { useReduxState } from '../../hooks/use-redux-state' import * as selectors from '../../redux/selectors' +import * as colors from '../../styles/colors' import { contentPadding } from '../../styles/variables' import { getIssueIconAndColor, @@ -29,6 +30,7 @@ import { NotificationCardHeader } from './partials/NotificationCardHeader' import { CommentRow } from './partials/rows/CommentRow' import { CommitRow } from './partials/rows/CommitRow' import { IssueOrPullRequestRow } from './partials/rows/IssueOrPullRequestRow' +import { PrivateNotificationRow } from './partials/rows/PrivateNotificationRow' import { ReleaseRow } from './partials/rows/ReleaseRow' import { RepositoryRow } from './partials/rows/RepositoryRow' @@ -48,10 +50,22 @@ const styles = StyleSheet.create({ export const NotificationCard = React.memo((props: NotificationCardProps) => { const { notification, onlyOneRepository, isSelected } = props + const repoFullName = + (notification && + (notification.repository.full_name || notification.repository.name)) || + '' + + const { owner: repoOwnerName, repo: repoName } = getOwnerAndRepo(repoFullName) + const itemRef = useRef(null) const springAnimatedTheme = useSpringAnimatedTheme() - const hasPrivateAccess = useReduxState( - selectors.githubHasPrivateAccessSelector, + + const hasPrivateAccess = useReduxState(state => + selectors.githubHasPrivateAccessToRepoSelector( + state, + repoOwnerName, + repoName, + ), ) useEffect( @@ -78,11 +92,13 @@ export const NotificationCard = React.memo((props: NotificationCardProps) => { const isSaved = saved === true const isPrivate = isNotificationPrivate(notification) - const title = trimNewLinesAndSpaces(subject.title) + const isPrivateAndCantSee = !!( + isPrivate && + !hasPrivateAccess && + !notification.enhanced + ) - const repoFullName = - notification.repository.full_name || notification.repository.name || '' - const { owner: repoOwnerName, repo: repoName } = getOwnerAndRepo(repoFullName) + const title = trimNewLinesAndSpaces(subject.title) const subjectType = subject.type || '' @@ -152,8 +168,10 @@ export const NotificationCard = React.memo((props: NotificationCardProps) => { const cardIconDetails = getNotificationIconAndColor(notification, (issue || pullRequest || undefined) as any) - const cardIconName = cardIconDetails.icon - const cardIconColor = cardIconDetails.color + const cardIconName = isPrivateAndCantSee ? 'lock' : cardIconDetails.icon + const cardIconColor = isPrivateAndCantSee + ? colors.yellow + : cardIconDetails.color const { icon: pullRequestIconName, color: pullRequestIconColor } = pullRequest ? getPullRequestIconAndColor(pullRequest as any) @@ -396,18 +414,17 @@ export const NotificationCard = React.memo((props: NotificationCardProps) => { /> )} - {!!(isPrivate && !hasPrivateAccess && !notification.enhanced) && ( - )} diff --git a/packages/components/src/components/cards/NotificationCards.tsx b/packages/components/src/components/cards/NotificationCards.tsx index 6db6f50b..d7492442 100644 --- a/packages/components/src/components/cards/NotificationCards.tsx +++ b/packages/components/src/components/cards/NotificationCards.tsx @@ -5,8 +5,8 @@ import { Column, constants, EnhancedGitHubNotification, + EnhancedLoadState, isItemRead, - LoadState, } from '@devhub/core' import useKeyPressCallback from '../../hooks/use-key-press-callback' import { useKeyboardScrolling } from '../../hooks/use-keyboard-scrolling' @@ -27,7 +27,7 @@ export interface NotificationCardsProps { columnIndex: number errorMessage: EmptyCardsProps['errorMessage'] fetchNextPage: (() => void) | undefined - loadState: LoadState + loadState: EnhancedLoadState notifications: EnhancedGitHubNotification[] refresh: EmptyCardsProps['refresh'] repoIsKnown?: boolean diff --git a/packages/components/src/components/cards/partials/rows/PrivateNotificationRow.tsx b/packages/components/src/components/cards/partials/rows/PrivateNotificationRow.tsx new file mode 100644 index 00000000..df62b319 --- /dev/null +++ b/packages/components/src/components/cards/partials/rows/PrivateNotificationRow.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { View } from 'react-native' + +import { constants } from '@devhub/core' +import { useCSSVariablesOrSpringAnimatedTheme } from '../../../../hooks/use-css-variables-or-spring--animated-theme' +import { SpringAnimatedText } from '../../../animated/spring/SpringAnimatedText' +import { Link } from '../../../common/Link' +import { cardStyles, getCardStylesForTheme } from '../../styles' +import { cardRowStyles } from './styles' + +export interface PrivateNotificationRowProps { + isRead?: boolean + ownerId?: number | string | undefined + repoId?: number | string | undefined + smallLeftColumn?: boolean +} + +export const PrivateNotificationRow = React.memo( + (props: PrivateNotificationRowProps) => { + const springAnimatedTheme = useCSSVariablesOrSpringAnimatedTheme() + + const { ownerId, repoId, smallLeftColumn } = props + + return ( + + + + + + + Install the GitHub App to unlock details from private + notifications. + + + + + ) + }, +) diff --git a/packages/components/src/components/columns/ColumnOptions.tsx b/packages/components/src/components/columns/ColumnOptions.tsx index 5a1c0c33..12878b2e 100644 --- a/packages/components/src/components/columns/ColumnOptions.tsx +++ b/packages/components/src/components/columns/ColumnOptions.tsx @@ -85,9 +85,6 @@ export const ColumnOptions = React.memo((props: ColumnOptionsProps) => { const springAnimatedTheme = useCSSVariablesOrSpringAnimatedTheme() const columnIds = useReduxState(selectors.columnIdsSelector) - const hasPrivateAccess = useReduxState( - selectors.githubHasPrivateAccessSelector, - ) const deleteColumn = useReduxAction(actions.deleteColumn) const moveColumn = useReduxAction(actions.moveColumn) @@ -429,20 +426,14 @@ export const ColumnOptions = React.memo((props: ColumnOptionsProps) => { })()} {(() => { - const isPrivateChecked = - column.type === 'notifications' - ? column.filters && column.filters.private === true - : hasPrivateAccess && - !(column.filters && column.filters.private === false) + const isPrivateChecked = !( + column.filters && column.filters.private === false + ) const isPublicChecked = !( column.filters && column.filters.private === true ) - const canShowPrivateContent = hasPrivateAccess // || column.type === 'notifications' - - if (!canShowPrivateContent && !isPrivateChecked) return null - const getFilterValue = ( showPublic?: boolean, showPrivate?: boolean, @@ -483,10 +474,7 @@ export const ColumnOptions = React.memo((props: ColumnOptionsProps) => { analyticsLabel="public" checked={isPublicChecked} containerStyle={{ flexGrow: 1 }} - disabled={ - isPublicChecked && - (!isPrivateChecked || !canShowPrivateContent) - } + disabled={isPublicChecked && !isPrivateChecked} label="Public" // labelIcon="globe" onChange={checked => { @@ -503,10 +491,7 @@ export const ColumnOptions = React.memo((props: ColumnOptionsProps) => { analyticsLabel="private" checked={isPrivateChecked} containerStyle={{ flexGrow: 1 }} - disabled={ - (isPrivateChecked && !isPublicChecked) || - (!isPrivateChecked && !canShowPrivateContent) - } + disabled={isPrivateChecked && !isPublicChecked} label="Private" // labelIcon="lock" onChange={checked => { diff --git a/packages/components/src/components/columns/EventOrNotificationColumn.tsx b/packages/components/src/components/columns/EventOrNotificationColumn.tsx index 29f78a6a..187c15ea 100644 --- a/packages/components/src/components/columns/EventOrNotificationColumn.tsx +++ b/packages/components/src/components/columns/EventOrNotificationColumn.tsx @@ -79,10 +79,6 @@ export const EventOrNotificationColumn = React.memo( ), ) - const hasPrivateAccess = useReduxState( - selectors.githubHasPrivateAccessSelector, - ) - const clearableItems = (filteredItems as any[]).filter( (item: EnhancedGitHubEvent | EnhancedGitHubNotification) => { return !!(item && !item.saved) /* && isItemRead(item) */ @@ -168,15 +164,15 @@ export const EventOrNotificationColumn = React.memo( const hasAnyFilter = column.type === 'notifications' - ? notificationColumnHasAnyFilter( - { ...column.filters, clearedAt: undefined }, - hasPrivateAccess, - ) + ? notificationColumnHasAnyFilter({ + ...column.filters, + clearedAt: undefined, + }) : column.type === 'activity' - ? activityColumnHasAnyFilter( - { ...column.filters, clearedAt: undefined }, - hasPrivateAccess, - ) + ? activityColumnHasAnyFilter({ + ...column.filters, + clearedAt: undefined, + }) : false // column doesnt have any filter, diff --git a/packages/components/src/components/modals/AddColumnModal.tsx b/packages/components/src/components/modals/AddColumnModal.tsx index a0b409b5..42917230 100644 --- a/packages/components/src/components/modals/AddColumnModal.tsx +++ b/packages/components/src/components/modals/AddColumnModal.tsx @@ -377,42 +377,6 @@ export function AddColumnModal(props: AddColumnModalProps) { - - - Coming soon: support for private repositories. Click here and - subscribe to this issue if you want to be notified. - - - - - Also, follow @devhub_app on Twitter! - - - diff --git a/packages/components/src/containers/EventCardsContainer.tsx b/packages/components/src/containers/EventCardsContainer.tsx index c8ceb20b..163f9524 100644 --- a/packages/components/src/containers/EventCardsContainer.tsx +++ b/packages/components/src/containers/EventCardsContainer.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useRef } from 'react' import { ActivityColumnSubscription, @@ -7,7 +7,6 @@ import { constants, EnhancedGitHubEvent, getOlderEventDate, - InstallationResponse, Omit, } from '@devhub/core' import { View } from 'react-native' @@ -17,6 +16,7 @@ import { GenericMessageWithButtonView } from '../components/cards/GenericMessage import { NoTokenView } from '../components/cards/NoTokenView' import { Button } from '../components/common/Button' import { Link } from '../components/common/Link' +import { useGitHubAPI } from '../hooks/use-github-api' import { useReduxAction } from '../hooks/use-redux-action' import { useReduxState } from '../hooks/use-redux-state' import { octokit } from '../libs/github' @@ -50,6 +50,10 @@ export const EventCardsContainer = React.memo( const data = (firstSubscription && firstSubscription.data) || {} + const isNotFound = (data.errorMessage || '') + .toLowerCase() + .includes('not found') + const owner = (firstSubscription && (('owner' in firstSubscription.params && @@ -57,14 +61,15 @@ export const EventCardsContainer = React.memo( ('org' in firstSubscription.params && firstSubscription.params.org))) || undefined - const repo = - (firstSubscription && - ('repo' in firstSubscription.params && - firstSubscription.params.repo)) || - undefined - // TODO: Get from redux state - const installationResponse = { owner, repo } as any + const ownerResponse = useGitHubAPI( + octokit.users.getByUsername, + isNotFound && owner ? { username: owner } : null, + ) + + const installationsLoadState = useReduxState( + selectors.installationsLoadStateSelector, + ) const fetchColumnSubscriptionRequest = useReduxAction( actions.fetchColumnSubscriptionRequest, @@ -166,14 +171,25 @@ export const EventCardsContainer = React.memo( return } - if ( - (firstSubscription.data.errorMessage || '') - .toLowerCase() - .includes('not found') - ) { + if (isNotFound) { if (!githubAppToken) return - if (installationResponse.ownerId) { + if ( + ownerResponse.loadingState === 'loading' || + installationsLoadState === 'loading' + ) { + return ( + + ) + } + + if (ownerResponse.data && ownerResponse.data.id) { return (