diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f5c55be26..0afb6b2fc 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,6 +1,6 @@ name: Run Tests -on: [push, pull_request] +on: [push] jobs: run-unit-tests: @@ -26,7 +26,10 @@ jobs: - name: Run Prettier (Check) run: yarn prettier:check + - name: Run Typechecking + run: yarn tsc --noEmit + - name: Run Jest - run: yarn test --coverage --runInBand + run: yarn test --coverage --runInBand --verbose - uses: codecov/codecov-action@v1 diff --git a/package.json b/package.json index 29d047b9c..a558fdbbe 100644 --- a/package.json +++ b/package.json @@ -37,11 +37,11 @@ "contributors": [ { "name": "Emmanouil Konstantinidis", - "url": "https://githib.com/manosim" + "url": "https://github.com/manosim" }, { "name": "Jake 'Sid' Smith", - "url": "https://githib.com/JakeSidSmith" + "url": "https://github.com/JakeSidSmith" } ], "license": "MIT", @@ -52,7 +52,7 @@ "jest": { "preset": "ts-jest/presets/js-with-ts", "setupFiles": [ - "/src/js/__helpers__/setupEnvVars.js" + "/src/__helpers__/setupEnvVars.js" ], "testEnvironment": "jsdom", "coverageThreshold": { @@ -106,12 +106,12 @@ "afterSign": "scripts/notarize.js" }, "dependencies": { - "@primer/octicons-react": "^11.1.0", + "@primer/octicons-react": "^11.2.0", "autoprefixer": "^10.1.0", - "axios": "=0.21.0", + "axios": "=0.21.1", "date-fns": "^2.16.1", "electron-updater": "^4.3.5", - "final-form": "^4.19.1", + "final-form": "^4.20.1", "lodash": "^4.17.20", "menubar": "^9.0.1", "nprogress": "=0.2.0", @@ -120,43 +120,35 @@ "react-dom": "=16.13.1", "react-emojione": "=5.0.1", "react-final-form": "^6.4.0", - "react-is": "^16.13.1", - "react-redux": "=7.2.0", - "react-router-dom": "^5.1.2", - "react-transition-group": "^4.3.0", + "react-router-dom": "^5.2.0", + "react-transition-group": "^4.4.1", "react-typist": "^2.0.5", - "redux": "=4.0.5", - "redux-storage": "=4.1.2", - "redux-storage-decorator-filter": "=1.1.8", - "redux-storage-engine-localstorage": "=1.1.4", - "redux-thunk": "=2.3.0", "tailwindcss": "^2.0.2", - "ts-loader": "^8.0.11", - "typescript": "^4.1.2" + "ts-loader": "^8.0.12", + "typescript": "^4.1.3" }, "devDependencies": { - "@testing-library/react": "^10.0.2", - "@types/jest": "^26.0.15", + "@testing-library/react": "^11.2.2", + "@testing-library/react-hooks": "^3.7.0", + "@types/jest": "^26.0.19", "@types/lodash": "^4.14.165", - "@types/node": "^14.14.9", + "@types/node": "^14.14.14", "@types/react": "^16.9.32", - "@types/react-redux": "^7.1.7", - "@types/react-transition-group": "^4.2.4", - "@types/styled-components": "^5.0.1", + "@types/react-router-dom": "^5.1.6", + "@types/react-transition-group": "^4.4.0", "css-loader": "^5.0.1", "electron": "^11.1.0", "electron-builder": "^22.9.1", "electron-notarize": "^1.0.0", "jest": "^26.6.3", - "nock": "^12.0.3", + "nock": "^13.0.5", "postcss-loader": "^4.1.0", - "prettier": "=2.2.0", + "prettier": "=2.2.1", "react-test-renderer": "=16.13.1", - "redux-mock-store": "=1.5.4", "style-loader": "^2.0.0", "ts-jest": "^26.4.4", - "webpack": "^5.6.0", + "webpack": "^5.11.0", "webpack-cli": "^4.2.0", - "webpack-merge": "^5.4.0" + "webpack-merge": "^5.7.3" } } diff --git a/src/js/__helpers__/setupEnvVars.js b/src/__helpers__/setupEnvVars.js similarity index 100% rename from src/js/__helpers__/setupEnvVars.js rename to src/__helpers__/setupEnvVars.js diff --git a/src/js/__mocks__/electron.js b/src/__mocks__/electron.js similarity index 100% rename from src/js/__mocks__/electron.js rename to src/__mocks__/electron.js diff --git a/src/__mocks__/mock-state.ts b/src/__mocks__/mock-state.ts new file mode 100644 index 000000000..6ce8ee858 --- /dev/null +++ b/src/__mocks__/mock-state.ts @@ -0,0 +1,20 @@ +import { Appearance, AuthState, SettingsState } from '../types'; + +export const mockAccounts: AuthState = { + token: 'token-123-456', + enterpriseAccounts: [ + { + token: 'token-gitify-123-456', + hostname: 'github.gitify.io', + }, + ], +}; + +export const mockSettings: SettingsState = { + participating: false, + playSound: true, + showNotifications: true, + markOnClick: false, + openAtStartup: false, + appearance: Appearance.SYSTEM, +}; diff --git a/src/js/__mocks__/mockedData.ts b/src/__mocks__/mockedData.ts similarity index 97% rename from src/js/__mocks__/mockedData.ts rename to src/__mocks__/mockedData.ts index 8a4391ab4..8b37380b3 100644 --- a/src/js/__mocks__/mockedData.ts +++ b/src/__mocks__/mockedData.ts @@ -1,5 +1,5 @@ -import { Notification, Repository } from '../../types/github'; -import { EnterpriseAccount } from '../../types/reducers'; +import { AccountNotifications, EnterpriseAccount } from '../types'; +import { Notification, Repository } from '../typesGithub'; export const mockedEnterpriseAccounts: EnterpriseAccount[] = [ { @@ -251,7 +251,7 @@ export const mockedEnterpriseNotifications = [ } as Notification, ]; -export const mockedNotificationsReducerData = [ +export const mockedAccountNotifications: AccountNotifications[] = [ { hostname: 'github.com', notifications: mockedGithubNotifications, @@ -261,3 +261,10 @@ export const mockedNotificationsReducerData = [ notifications: mockedEnterpriseNotifications, }, ]; + +export const mockedSingleAccountNotifications: AccountNotifications[] = [ + { + hostname: 'github.com', + notifications: [mockedSingleNotification], + }, +]; diff --git a/src/js/app.tsx b/src/app.tsx similarity index 52% rename from src/js/app.tsx rename to src/app.tsx index f6223fb53..175cdc0f7 100644 --- a/src/js/app.tsx +++ b/src/app.tsx @@ -1,35 +1,27 @@ -import * as React from 'react'; +import React, { useContext } from 'react'; import { Redirect, HashRouter as Router, Route, Switch, } from 'react-router-dom'; -import { Provider } from 'react-redux'; -import configureStore from './store/configureStore'; -import Loading from './components/loading'; -import Sidebar from './components/sidebar'; - -import EnterpriseLoginRoute from './routes/enterprise-login'; -import LoginRoute from './routes/login'; -import NotificationsRoute from './routes/notifications'; -import SettingsRoute from './routes/settings'; - -// Store -export const store = configureStore(); +import { AppContext, AppProvider } from './context/App'; +import { Loading } from './components/Loading'; +import { LoginEnterpriseRoute } from './routes/LoginEnterprise'; +import { LoginRoute } from './routes/Login'; +import { NotificationsRoute } from './routes/Notifications'; +import { SettingsRoute } from './routes/Settings'; +import { Sidebar } from './components/Sidebar'; export const PrivateRoute = ({ component: Component, ...rest }) => { - // @ts-ignore - const authReducer = store.getState().auth; - const isAuthenticated = - authReducer.token !== null || authReducer.enterpriseAccounts.length > 0; + const { isLoggedIn } = useContext(AppContext); return ( - isAuthenticated ? ( + isLoggedIn ? ( ) : ( { export const App = () => { return ( - +
@@ -53,10 +45,10 @@ export const App = () => { - +
-
+ ); }; diff --git a/src/js/components/account-notifications.test.tsx b/src/components/AccountNotifications.test.tsx similarity index 70% rename from src/js/components/account-notifications.test.tsx rename to src/components/AccountNotifications.test.tsx index 39c40ed79..088fbfc4b 100644 --- a/src/js/components/account-notifications.test.tsx +++ b/src/components/AccountNotifications.test.tsx @@ -1,12 +1,14 @@ -import * as React from 'react'; -import * as TestRendener from 'react-test-renderer'; +import React from 'react'; +import TestRendener from 'react-test-renderer'; -import { AccountNotifications } from './account-notifications'; +import { AccountNotifications } from './AccountNotifications'; import { mockedGithubNotifications } from '../__mocks__/mockedData'; -jest.mock('./repository'); +jest.mock('./Repository', () => ({ + RepositoryNotifications: () =>
Repository
, +})); -describe('components/account-notifications.tsx', () => { +describe('components/AccountNotifications.tsx', () => { it('should render itself (github.com with notifications)', () => { const props = { hostname: 'github.com', diff --git a/src/js/components/account-notifications.tsx b/src/components/AccountNotifications.tsx similarity index 87% rename from src/js/components/account-notifications.tsx rename to src/components/AccountNotifications.tsx index bcf66346d..bd2e9e82f 100644 --- a/src/js/components/account-notifications.tsx +++ b/src/components/AccountNotifications.tsx @@ -1,9 +1,9 @@ -import * as React from 'react'; -import * as _ from 'lodash'; +import React from 'react'; +import _ from 'lodash'; import { ChevronDownIcon, ChevronLeftIcon } from '@primer/octicons-react'; -import { Notification } from '../../types/github'; -import RepositoryNotifications from './repository'; +import { Notification } from '../typesGithub'; +import { RepositoryNotifications } from './Repository'; interface IProps { hostname: string; diff --git a/src/js/components/all-read.test.tsx b/src/components/AllRead.test.tsx similarity index 82% rename from src/js/components/all-read.test.tsx rename to src/components/AllRead.test.tsx index 6af7247e5..5e7115e81 100644 --- a/src/js/components/all-read.test.tsx +++ b/src/components/AllRead.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; +import { Constants } from '../utils/constants'; -import Constants from '../utils/constants'; -import { AllRead } from './all-read'; +import { AllRead } from './AllRead'; jest.mock('react-typist'); diff --git a/src/js/components/all-read.tsx b/src/components/AllRead.tsx similarity index 73% rename from src/js/components/all-read.tsx rename to src/components/AllRead.tsx index a6b28fed7..b58271508 100644 --- a/src/js/components/all-read.tsx +++ b/src/components/AllRead.tsx @@ -2,21 +2,21 @@ import * as React from 'react'; import Typist from 'react-typist'; import { emojify } from 'react-emojione'; -import constants from '../utils/constants'; +import { Constants } from '../utils/constants'; export const AllRead = () => { const message = React.useMemo( () => - constants.ALLREAD_MESSAGES[ - Math.floor(Math.random() * constants.ALLREAD_MESSAGES.length) + Constants.ALLREAD_MESSAGES[ + Math.floor(Math.random() * Constants.ALLREAD_MESSAGES.length) ], [] ); const emoji = React.useMemo( () => - constants.ALLREAD_EMOJIS[ - Math.floor(Math.random() * constants.ALLREAD_EMOJIS.length) + Constants.ALLREAD_EMOJIS[ + Math.floor(Math.random() * Constants.ALLREAD_EMOJIS.length) ], [] ); diff --git a/src/components/Loading.test.tsx b/src/components/Loading.test.tsx new file mode 100644 index 000000000..98a8aa3a4 --- /dev/null +++ b/src/components/Loading.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import NProgress from 'nprogress'; + +import { AppContext } from '../context/App'; +import { Loading } from './Loading'; + +jest.mock('nprogress', () => { + return { + configure: jest.fn(), + start: jest.fn(), + done: jest.fn(), + remove: jest.fn(), + }; +}); + +describe('components/Loading.js', () => { + beforeEach(() => { + NProgress.configure.mockReset(); + NProgress.start.mockReset(); + NProgress.done.mockReset(); + NProgress.remove.mockReset(); + }); + + it('should check that NProgress is getting called in when isFetching changes (loading)', () => { + const { container } = render( + + + + ); + + expect(container.innerHTML).toBe(''); + expect(NProgress.configure).toHaveBeenCalledTimes(1); + expect(NProgress.start).toHaveBeenCalledTimes(1); + }); + + it('should check that NProgress is getting called in when isFetching changes (not loading)', () => { + const { container } = render( + + + + ); + + expect(container.innerHTML).toBe(''); + expect(NProgress.configure).toHaveBeenCalledTimes(1); + expect(NProgress.done).toHaveBeenCalledTimes(1); + }); + + it('should remove NProgress on unmount', () => { + const { unmount } = render( + + + + ); + expect(NProgress.remove).toHaveBeenCalledTimes(0); + unmount(); + expect(NProgress.remove).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx new file mode 100644 index 000000000..3ecd92caa --- /dev/null +++ b/src/components/Loading.tsx @@ -0,0 +1,28 @@ +import { useContext, useEffect } from 'react'; +import NProgress from 'nprogress'; + +import { AppContext } from '../context/App'; + +export const Loading = () => { + const { isFetching } = useContext(AppContext); + + useEffect(() => { + NProgress.configure({ + showSpinner: false, + }); + + return () => { + NProgress.remove(); + }; + }, []); + + useEffect(() => { + if (isFetching) { + NProgress.start(); + } else { + NProgress.done(); + } + }, [isFetching]); + + return null; +}; diff --git a/src/js/components/ui/logo.test.tsx b/src/components/Logo.test.tsx similarity index 95% rename from src/js/components/ui/logo.test.tsx rename to src/components/Logo.test.tsx index a4c3ea50a..2821b090e 100644 --- a/src/js/components/ui/logo.test.tsx +++ b/src/components/Logo.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import * as TestRenderer from 'react-test-renderer'; import { fireEvent, render } from '@testing-library/react'; -import { Logo } from './logo'; +import { Logo } from './Logo'; describe('components/ui/logo.tsx', () => { it('renders correctly (light)', () => { diff --git a/src/js/components/ui/logo.tsx b/src/components/Logo.tsx similarity index 100% rename from src/js/components/ui/logo.tsx rename to src/components/Logo.tsx diff --git a/src/components/NotificationRow.test.tsx b/src/components/NotificationRow.test.tsx new file mode 100644 index 000000000..b38da7a8f --- /dev/null +++ b/src/components/NotificationRow.test.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import * as TestRenderer from 'react-test-renderer'; +import { fireEvent, render } from '@testing-library/react'; + +const { shell } = require('electron'); + +import { AppContext } from '../context/App'; +import { mockedSingleNotification } from '../__mocks__/mockedData'; +import { NotificationRow } from './NotificationRow'; +import { mockSettings } from '../__mocks__/mock-state'; + +describe('components/Notification.js', () => { + beforeEach(() => { + spyOn(shell, 'openExternal'); + }); + + it('should render itself & its children', async () => { + (global as any).Date.now = jest.fn(() => new Date('2014')); + + const props = { + notification: mockedSingleNotification, + hostname: 'github.com', + }; + + const tree = TestRenderer.create(); + expect(tree).toMatchSnapshot(); + }); + + it('should open a notification in the browser', () => { + const markNotification = jest.fn(); + + const props = { + notification: mockedSingleNotification, + hostname: 'github.com', + }; + + const { getByRole } = render( + + + + ); + + fireEvent.click(getByRole('main')); + expect(shell.openExternal).toHaveBeenCalledTimes(1); + }); + + it('should open a notification in browser & mark it as read', () => { + const markNotification = jest.fn(); + + const props = { + notification: mockedSingleNotification, + hostname: 'github.com', + }; + + const { getByRole } = render( + + + + ); + + fireEvent.click(getByRole('main')); + expect(shell.openExternal).toHaveBeenCalledTimes(1); + expect(markNotification).toHaveBeenCalledTimes(1); + }); + + it('should mark a notification as read', () => { + const markNotification = jest.fn(); + + const props = { + notification: mockedSingleNotification, + hostname: 'github.com', + }; + + const { getByTitle } = render( + + + + + + ); + + fireEvent.click(getByTitle('Mark as Read')); + expect(markNotification).toHaveBeenCalledTimes(1); + }); + + it('should unsubscribe from a notification thread', () => { + const unsubscribeNotification = jest.fn(); + + const props = { + notification: mockedSingleNotification, + hostname: 'github.com', + }; + + const { getByLabelText } = render( + + + + + + ); + fireEvent.click(getByLabelText('Unsubscribe')); + expect(unsubscribeNotification).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/js/components/notification.tsx b/src/components/NotificationRow.tsx similarity index 64% rename from src/js/components/notification.tsx rename to src/components/NotificationRow.tsx index 04e63c1ee..12d955dc4 100644 --- a/src/js/components/notification.tsx +++ b/src/components/NotificationRow.tsx @@ -1,55 +1,49 @@ const { shell } = require('electron'); -import * as React from 'react'; -import { connect } from 'react-redux'; +import React, { useCallback, useContext } from 'react'; import { formatDistanceToNow, parseISO } from 'date-fns'; import { CheckIcon, MuteIcon } from '@primer/octicons-react'; -import { AppState } from '../../types/reducers'; import { formatReason, getNotificationTypeIcon } from '../utils/github-api'; import { generateGitHubWebUrl } from '../utils/helpers'; -import { markNotification, unsubscribeNotification } from '../actions'; -import { Notification } from '../../types/github'; +import { Notification } from '../typesGithub'; +import { AppContext } from '../context/App'; interface IProps { hostname: string; notification: Notification; - markOnClick: boolean; - markNotification: (id: string, hostname: string) => void; - unsubscribeNotification?: (id: string, hostname: string) => void; } -export const NotificationItem: React.FC = (props) => { - const pressTitle = () => { +export const NotificationRow: React.FC = ({ + notification, + hostname, +}) => { + const { settings } = useContext(AppContext); + const { markNotification, unsubscribeNotification } = useContext(AppContext); + + const pressTitle = useCallback(() => { openBrowser(); - if (props.markOnClick) { - markAsRead(); + if (settings.markOnClick) { + markNotification(notification.id, hostname); } - }; + }, [settings]); - const openBrowser = () => { + const openBrowser = useCallback(() => { // Some Notification types from GitHub are missing urls in their subjects. - if (props.notification.subject.url) { - const url = generateGitHubWebUrl(props.notification.subject.url); + if (notification.subject.url) { + const url = generateGitHubWebUrl(notification.subject.url); shell.openExternal(url); } - }; - - const markAsRead = () => { - const { hostname, notification } = props; - props.markNotification(notification.id, hostname); - }; + }, [notification]); const unsubscribe = (event: React.MouseEvent) => { // Don't trigger onClick of parent element. event.stopPropagation(); - const { hostname, notification } = props; - props.unsubscribeNotification(notification.id, hostname); + unsubscribeNotification(notification.id, hostname); }; - const { notification } = props; const reason = formatReason(notification.reason); const NotificationIcon = getNotificationTypeIcon(notification.subject.type); const updatedAt = formatDistanceToNow(parseISO(notification.updated_at), { @@ -77,7 +71,7 @@ export const NotificationItem: React.FC = (props) => { + + + + )} + +
+ +
+ + + ); +}; diff --git a/src/components/__snapshots__/AccountNotifications.test.tsx.snap b/src/components/__snapshots__/AccountNotifications.test.tsx.snap new file mode 100644 index 000000000..18c3181df --- /dev/null +++ b/src/components/__snapshots__/AccountNotifications.test.tsx.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/AccountNotifications.tsx should render itself (github.com with notifications) 1`] = ` +Array [ +
+ github.com +
, +
+ Repository +
, +] +`; + +exports[`components/AccountNotifications.tsx should render itself (github.com without notifications) 1`] = ` +
+ github.com +
+`; diff --git a/src/js/components/__snapshots__/all-read.test.tsx.snap b/src/components/__snapshots__/AllRead.test.tsx.snap similarity index 100% rename from src/js/components/__snapshots__/all-read.test.tsx.snap rename to src/components/__snapshots__/AllRead.test.tsx.snap diff --git a/src/js/components/ui/__snapshots__/logo.test.tsx.snap b/src/components/__snapshots__/Logo.test.tsx.snap similarity index 100% rename from src/js/components/ui/__snapshots__/logo.test.tsx.snap rename to src/components/__snapshots__/Logo.test.tsx.snap diff --git a/src/js/components/__snapshots__/notification.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap similarity index 98% rename from src/js/components/__snapshots__/notification.test.tsx.snap rename to src/components/__snapshots__/NotificationRow.test.tsx.snap index 927946e73..e9728a966 100644 --- a/src/js/components/__snapshots__/notification.test.tsx.snap +++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/notification.js should render itself & its children 1`] = ` +exports[`components/Notification.js should render itself & its children 1`] = `
diff --git a/src/js/components/__snapshots__/oops.test.tsx.snap b/src/components/__snapshots__/Oops.test.tsx.snap similarity index 100% rename from src/js/components/__snapshots__/oops.test.tsx.snap rename to src/components/__snapshots__/Oops.test.tsx.snap diff --git a/src/js/components/__snapshots__/repository.test.tsx.snap b/src/components/__snapshots__/Repository.test.tsx.snap similarity index 85% rename from src/js/components/__snapshots__/repository.test.tsx.snap rename to src/components/__snapshots__/Repository.test.tsx.snap index b00ba7351..d6deac116 100644 --- a/src/js/components/__snapshots__/repository.test.tsx.snap +++ b/src/components/__snapshots__/Repository.test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/repository.tsx should render itself & its children 1`] = ` +exports[`components/Repository.tsx should render itself & its children 1`] = ` Array [
, -
, +
+
+ NotificationRow +
+
+ NotificationRow +
+
, ] `; diff --git a/src/js/components/__snapshots__/sidebar.test.tsx.snap b/src/components/__snapshots__/Sidebar.test.tsx.snap similarity index 70% rename from src/js/components/__snapshots__/sidebar.test.tsx.snap rename to src/components/__snapshots__/Sidebar.test.tsx.snap index 90a4a5cf4..e368485e8 100644 --- a/src/js/components/__snapshots__/sidebar.test.tsx.snap +++ b/src/components/__snapshots__/Sidebar.test.tsx.snap @@ -81,60 +81,6 @@ exports[`components/Sidebar.tsx should render itself & its children (logged in)
- -
{ const props = { diff --git a/src/js/components/fields/radiogroup.tsx b/src/components/fields/RadioGroup.tsx similarity index 96% rename from src/js/components/fields/radiogroup.tsx rename to src/components/fields/RadioGroup.tsx index ce3d772a1..f61bf79ef 100644 --- a/src/js/components/fields/radiogroup.tsx +++ b/src/components/fields/RadioGroup.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { RadioGroupItem } from '../../../types'; +import { RadioGroupItem } from '../../types'; export const FieldRadioGroup = ({ label, diff --git a/src/js/components/fields/__snapshots__/radiogroup.test.tsx.snap b/src/components/fields/__snapshots__/RadioGroup.test.tsx.snap similarity index 100% rename from src/js/components/fields/__snapshots__/radiogroup.test.tsx.snap rename to src/components/fields/__snapshots__/RadioGroup.test.tsx.snap diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx new file mode 100644 index 000000000..fd2587c71 --- /dev/null +++ b/src/context/App.test.tsx @@ -0,0 +1,237 @@ +import React, { useContext } from 'react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; + +import { AppContext, AppProvider } from './App'; +import { AuthState, SettingsState } from '../types'; +import { mockAccounts, mockSettings } from '../__mocks__/mock-state'; +import { useNotifications } from '../hooks/useNotifications'; +import * as comms from '../utils/comms'; +import * as storage from '../utils/storage'; + +jest.mock('../hooks/useNotifications'); + +const customRender = ( + ui, + accounts: AuthState = mockAccounts, + settings: SettingsState = mockSettings +) => { + return render( + + {ui} + + ); +}; + +describe('context/App.tsx', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + describe('api methods', () => { + const updateSettingMock = jest.fn(); + + const fetchNotificationsMock = jest.fn(); + const markNotificationMock = jest.fn(); + const unsubscribeNotificationMock = jest.fn(); + const markRepoNotificationsMock = jest.fn(); + + (useNotifications as jest.Mock).mockReturnValue({ + fetchNotifications: fetchNotificationsMock, + markNotification: markNotificationMock, + unsubscribeNotification: unsubscribeNotificationMock, + markRepoNotifications: markRepoNotificationsMock, + }); + + beforeEach(() => { + updateSettingMock.mockReset(); + fetchNotificationsMock.mockReset(); + markNotificationMock.mockReset(); + unsubscribeNotificationMock.mockReset(); + markRepoNotificationsMock.mockReset(); + }); + + it('fetch notifications every minute', async () => { + customRender(null); + + waitFor(() => expect(fetchNotificationsMock).toHaveBeenCalledTimes(2)); + + fetchNotificationsMock.mockReset(); + + act(() => jest.advanceTimersByTime(60000)); + expect(fetchNotificationsMock).toHaveBeenCalledTimes(1); + + act(() => jest.advanceTimersByTime(60000)); + expect(fetchNotificationsMock).toHaveBeenCalledTimes(2); + + act(() => jest.advanceTimersByTime(60000)); + expect(fetchNotificationsMock).toHaveBeenCalledTimes(3); + }); + + it('should call fetchNotifications', async () => { + const TestComponent = () => { + const { fetchNotifications } = useContext(AppContext); + + return ; + }; + + const { getByText } = customRender(); + + fetchNotificationsMock.mockReset(); + + fireEvent.click(getByText('Test Case')); + + expect(fetchNotificationsMock).toHaveBeenCalled(); + expect(fetchNotificationsMock).toHaveBeenCalledTimes(1); + }); + + it('should call markNotification', async () => { + const TestComponent = () => { + const { markNotification } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + markNotificationMock.mockReset(); + + fireEvent.click(getByText('Test Case')); + + expect(markNotificationMock).toHaveBeenCalledTimes(1); + expect(markNotificationMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + '123-456', + 'github.com' + ); + }); + + it('should call unsubscribeNotification', async () => { + const TestComponent = () => { + const { unsubscribeNotification } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + unsubscribeNotificationMock.mockReset(); + + fireEvent.click(getByText('Test Case')); + + expect(unsubscribeNotificationMock).toHaveBeenCalledTimes(1); + expect(unsubscribeNotificationMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + '123-456', + 'github.com' + ); + }); + + it('should call markRepoNotifications', async () => { + const TestComponent = () => { + const { markRepoNotifications } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + markRepoNotificationsMock.mockReset(); + + fireEvent.click(getByText('Test Case')); + + expect(markRepoNotificationsMock).toHaveBeenCalledTimes(1); + expect(markRepoNotificationsMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + 'manosim/gitify', + 'github.com' + ); + }); + }); + + it('should call updateSetting', async () => { + const saveStateMock = jest.spyOn(storage, 'saveState'); + + const TestComponent = () => { + const { updateSetting } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + act(() => { + fireEvent.click(getByText('Test Case')); + }); + + expect(saveStateMock).toHaveBeenCalled(); + expect(saveStateMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + { + appearance: 'SYSTEM', + markOnClick: false, + openAtStartup: false, + participating: true, + playSound: true, + showNotifications: true, + } + ); + }); + + it('should call updateSetting and set auto launch(openAtStartup)', async () => { + const setAutoLaunchMock = jest.spyOn(comms, 'setAutoLaunch'); + const saveStateMock = jest.spyOn(storage, 'saveState'); + + const TestComponent = () => { + const { updateSetting } = useContext(AppContext); + + return ( + + ); + }; + + const { getByText } = customRender(); + + act(() => { + fireEvent.click(getByText('Test Case')); + }); + + expect(setAutoLaunchMock).toHaveBeenCalled(); + expect(setAutoLaunchMock).toHaveBeenCalledWith(true); + expect(saveStateMock).toHaveBeenCalled(); + expect(saveStateMock).toHaveBeenCalledWith( + { enterpriseAccounts: [], token: null }, + { + appearance: 'SYSTEM', + markOnClick: false, + openAtStartup: true, + participating: false, + playSound: true, + showNotifications: true, + } + ); + }); +}); diff --git a/src/context/App.tsx b/src/context/App.tsx new file mode 100644 index 000000000..13dfa37ef --- /dev/null +++ b/src/context/App.tsx @@ -0,0 +1,189 @@ +import React, { + useState, + createContext, + useCallback, + useEffect, + useMemo, +} from 'react'; + +import { + AccountNotifications, + Appearance, + AuthOptions, + AuthState, + SettingsState, +} from '../types'; +import { authGitHub, getToken } from '../utils/auth'; +import { clearState, loadState, saveState } from '../utils/storage'; +import { setAppearance } from '../utils/appearance'; +import { setAutoLaunch } from '../utils/comms'; +import { useInterval } from '../hooks/useInterval'; +import { useNotifications } from '../hooks/useNotifications'; + +const defaultAccounts: AuthState = { + token: null, + enterpriseAccounts: [], +}; + +export const defaultSettings: SettingsState = { + participating: false, + playSound: true, + showNotifications: true, + markOnClick: false, + openAtStartup: false, + appearance: Appearance.SYSTEM, +}; + +interface AppContextState { + accounts: AuthState; + isLoggedIn: boolean; + login: () => void; + loginEnterprise: (data: AuthOptions) => void; + logout: () => void; + + notifications: AccountNotifications[]; + isFetching: boolean; + requestFailed: boolean; + fetchNotifications: () => Promise; + markNotification: (id: string, hostname: string) => Promise; + unsubscribeNotification: (id: string, hostname: string) => Promise; + markRepoNotifications: (id: string, hostname: string) => Promise; + + settings: SettingsState; + updateSetting: (name: keyof SettingsState, value: any) => void; +} + +export const AppContext = createContext>({}); + +export const AppProvider = ({ children }: { children: React.ReactNode }) => { + const [accounts, setAccounts] = useState(defaultAccounts); + const [settings, setSettings] = useState(defaultSettings); + const { + fetchNotifications, + notifications, + requestFailed, + isFetching, + markNotification, + unsubscribeNotification, + markRepoNotifications, + } = useNotifications(); + + useEffect(() => { + restoreSettings(); + }, []); + + useEffect(() => { + setAppearance(settings.appearance as Appearance); + }, [settings.appearance]); + + useEffect(() => { + fetchNotifications(accounts, settings); + }, [settings.participating]); + + useEffect(() => { + fetchNotifications(accounts, settings); + }, [accounts.token, accounts.enterpriseAccounts.length]); + + useInterval(() => { + fetchNotifications(accounts, settings); + }, 60000); + + const updateSetting = useCallback( + (name: keyof SettingsState, value: boolean | Appearance) => { + if (name === 'openAtStartup') { + setAutoLaunch(value as boolean); + } + + const newSettings = { ...settings, [name]: value }; + setSettings(newSettings); + saveState(accounts, newSettings); + }, + [accounts, settings] + ); + + const isLoggedIn = useMemo(() => { + return !!accounts.token || accounts.enterpriseAccounts.length > 0; + }, [accounts]); + + const login = useCallback(async () => { + const { authCode } = await authGitHub(); + const { token } = await getToken(authCode); + setAccounts({ ...accounts, token }); + saveState({ ...accounts, token }, settings); + }, [accounts]); + + const loginEnterprise = useCallback( + async (data: AuthOptions) => { + const { authOptions, authCode } = await authGitHub(data); + const { token } = await getToken(authCode, authOptions); + setAccounts({ ...accounts, token }); + saveState({ ...accounts, token }, settings); + }, + [accounts] + ); + + const logout = useCallback(() => { + setAccounts(defaultAccounts); + clearState(); + }, []); + + const restoreSettings = useCallback(() => { + const existing = loadState(); + + if (existing.accounts) { + setAccounts({ ...defaultAccounts, ...existing.accounts }); + } + + if (existing.settings) { + setSettings({ ...defaultSettings, ...existing.settings }); + } + }, []); + + const fetchNotificationsWithAccounts = useCallback( + async () => await fetchNotifications(accounts, settings), + [accounts, settings, notifications] + ); + + const markNotificationWithAccounts = useCallback( + async (id: string, hostname: string) => + await markNotification(accounts, id, hostname), + [accounts, notifications] + ); + + const unsubscribeNotificationWithAccounts = useCallback( + async (id: string, hostname: string) => + await unsubscribeNotification(accounts, id, hostname), + [accounts, notifications] + ); + + const markRepoNotificationsWithAccounts = useCallback( + async (repoSlug: string, hostname: string) => + await markRepoNotifications(accounts, repoSlug, hostname), + [accounts, notifications] + ); + + return ( + + {children} + + ); +}; diff --git a/src/hooks/useInterval.ts b/src/hooks/useInterval.ts new file mode 100644 index 000000000..c4d7db5ea --- /dev/null +++ b/src/hooks/useInterval.ts @@ -0,0 +1,25 @@ +import { useEffect, useRef } from 'react'; + +// Thanks to https://overreacted.io/making-setinterval-declarative-with-react-hooks/ + +export const useInterval = (callback, delay) => { + const savedCallback = useRef(); + + // Remember the latest callback. + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + // Set up the interval. + useEffect(() => { + function tick() { + // @ts-ignore + savedCallback.current(); + } + + if (delay !== null) { + let id = setInterval(tick, delay); + return () => clearInterval(id); + } + }, [delay]); +}; diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts new file mode 100644 index 000000000..9a11ed238 --- /dev/null +++ b/src/hooks/useNotifications.test.ts @@ -0,0 +1,477 @@ +import axios from 'axios'; +import nock from 'nock'; +import { act, renderHook } from '@testing-library/react-hooks'; + +import { mockAccounts, mockSettings } from '../__mocks__/mock-state'; +import { useNotifications } from './useNotifications'; +import { AuthState } from '../types'; + +describe('hooks/useNotifications.ts', () => { + beforeEach(() => { + axios.defaults.adapter = require('axios/lib/adapters/http'); + }); + + describe('fetchNotifications', () => { + describe('github.com & enterprise', () => { + it('should fetch notifications with success - github.com & enterprise', async () => { + const notifications = [ + { id: 1, title: 'This is a notification.' }, + { id: 2, title: 'This is another one.' }, + ]; + + nock('https://api.github.com') + .get('/notifications?participating=false') + .reply(200, notifications); + + nock('https://github.gitify.io/api/v3') + .get('/notifications?participating=false') + .reply(200, notifications); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.fetchNotifications(mockAccounts, mockSettings); + }); + + expect(result.current.isFetching).toBe(true); + + await waitForNextUpdate(); + + expect(result.current.isFetching).toBe(false); + expect(result.current.notifications[0].hostname).toBe( + 'github.gitify.io' + ); + expect(result.current.notifications[1].hostname).toBe('github.com'); + }); + + it('should fetch notifications with failure - github.com & enterprise', async () => { + const message = 'Oops! Something went wrong.'; + + nock('https://api.github.com/') + .get('/notifications?participating=false') + .reply(400, { message }); + + nock('https://github.gitify.io/api/v3/') + .get('/notifications?participating=false') + .reply(400, { message }); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.fetchNotifications(mockAccounts, mockSettings); + }); + + expect(result.current.isFetching).toBe(true); + + await waitForNextUpdate(); + + expect(result.current.isFetching).toBe(false); + expect(result.current.requestFailed).toBe(true); + }); + }); + + describe('enterprise', () => { + it('should fetch notifications with success - enterprise only', async () => { + const accounts: AuthState = { + ...mockAccounts, + token: null, + }; + + const notifications = [ + { id: 1, title: 'This is a notification.' }, + { id: 2, title: 'This is another one.' }, + ]; + + nock('https://github.gitify.io/api/v3/') + .get('/notifications?participating=false') + .reply(200, notifications); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.fetchNotifications(accounts, mockSettings); + }); + + await waitForNextUpdate(); + + expect(result.current.notifications[0].hostname).toBe( + 'github.gitify.io' + ); + expect(result.current.notifications[0].notifications.length).toBe(2); + }); + + it('should fetch notifications with failure - enterprise only', async () => { + const accounts: AuthState = { + ...mockAccounts, + token: null, + }; + + nock('https://github.gitify.io/api/v3/') + .get('/notifications?participating=false') + .reply(400, { message: 'Oops! Something went wrong.' }); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.fetchNotifications(accounts, mockSettings); + }); + + await waitForNextUpdate(); + + expect(result.current.requestFailed).toBe(true); + }); + }); + + describe('github.com', () => { + it('should fetch notifications with success - github.com only', async () => { + const accounts: AuthState = { + ...mockAccounts, + enterpriseAccounts: [], + }; + + const notifications = [ + { id: 1, title: 'This is a notification.' }, + { id: 2, title: 'This is another one.' }, + ]; + + nock('https://api.github.com') + .get('/notifications?participating=false') + .reply(200, notifications); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.fetchNotifications(accounts, mockSettings); + }); + + await waitForNextUpdate(); + + expect(result.current.notifications[0].hostname).toBe('github.com'); + expect(result.current.notifications[0].notifications.length).toBe(2); + }); + + it('should fetch notifications with failure - github.com only', async () => { + const accounts: AuthState = { + ...mockAccounts, + enterpriseAccounts: [], + }; + + nock('https://api.github.com/') + .get('/notifications?participating=false') + .reply(400, { message: 'Oops! Something went wrong.' }); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.fetchNotifications(accounts, mockSettings); + }); + + await waitForNextUpdate(); + + expect(result.current.requestFailed).toBe(true); + }); + }); + }); + + describe('markNotification', () => { + const id = 'notification-123'; + + describe('github.com', () => { + const accounts = { ...mockAccounts, enterpriseAccounts: [] }; + const hostname = 'github.com'; + + it('should mark a notification as read with success - github.com', async () => { + nock('https://api.github.com/') + .patch(`/notifications/threads/${id}`) + .reply(200); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.markNotification(accounts, id, hostname); + }); + + await waitForNextUpdate(); + + expect(result.current.notifications.length).toBe(0); + }); + + it('should mark a notification as read with failure - github.com', async () => { + nock('https://api.github.com/') + .patch(`/notifications/threads/${id}`) + .reply(400); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.markNotification(accounts, id, hostname); + }); + + await waitForNextUpdate(); + + expect(result.current.notifications.length).toBe(0); + }); + }); + + describe('enterprise', () => { + const accounts = { ...mockAccounts, token: null }; + const hostname = 'github.gitify.io'; + + it('should mark a notification as read with success - enterprise', async () => { + nock('https://github.gitify.io/') + .patch(`/notifications/threads/${id}`) + .reply(200); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.markNotification(accounts, id, hostname); + }); + + await waitForNextUpdate(); + + expect(result.current.notifications.length).toBe(0); + }); + + it('should mark a notification as read with failure - enterprise', async () => { + nock('https://github.gitify.io/') + .patch(`/notifications/threads/${id}`) + .reply(400); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.markNotification(accounts, id, hostname); + }); + + await waitForNextUpdate(); + + expect(result.current.notifications.length).toBe(0); + }); + }); + }); + + describe('unsubscribeNotification', () => { + const id = 'notification-123'; + + describe('github.com', () => { + const accounts = { ...mockAccounts, enterpriseAccounts: [] }; + const hostname = 'github.com'; + + it('should unsubscribe from a notification with success - github.com', async () => { + // The unsubscribe endpoint call. + nock('https://api.github.com/') + .put(`/notifications/threads/${id}/subscription`) + .reply(200); + + // The mark read endpoint call. + nock('https://api.github.com/') + .patch(`/notifications/threads/${id}`) + .reply(200); + + const { result, waitForValueToChange } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.unsubscribeNotification(accounts, id, hostname); + }); + + await waitForValueToChange(() => { + return result.current.isFetching; + }); + + expect(result.current.notifications.length).toBe(0); + }); + + it('should unsubscribe from a notification with failure - github.com', async () => { + // The unsubscribe endpoint call. + nock('https://api.github.com/') + .put(`/notifications/threads/${id}/subscription`) + .reply(400); + + // The mark read endpoint call. + nock('https://api.github.com/') + .patch(`/notifications/threads/${id}`) + .reply(400); + + const { result, waitForValueToChange } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.unsubscribeNotification(accounts, id, hostname); + }); + + await waitForValueToChange(() => { + return result.current.isFetching; + }); + + expect(result.current.notifications.length).toBe(0); + }); + }); + + describe('enterprise', () => { + const accounts = { ...mockAccounts, token: null }; + const hostname = 'github.gitify.io'; + + it('should unsubscribe from a notification with success - enterprise', async () => { + // The unsubscribe endpoint call. + nock('https://github.gitify.io/') + .put(`/notifications/threads/${id}/subscription`) + .reply(200); + + // The mark read endpoint call. + nock('https://github.gitify.io/') + .patch(`/notifications/threads/${id}`) + .reply(200); + + const { result, waitForValueToChange } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.unsubscribeNotification(accounts, id, hostname); + }); + + await waitForValueToChange(() => { + return result.current.isFetching; + }); + + expect(result.current.notifications.length).toBe(0); + }); + + it('should unsubscribe from a notification with failure - enterprise', async () => { + // The unsubscribe endpoint call. + nock('https://github.gitify.io/') + .put(`/notifications/threads/${id}/subscription`) + .reply(400); + + // The mark read endpoint call. + nock('https://github.gitify.io/') + .patch(`/notifications/threads/${id}`) + .reply(400); + + const { result, waitForValueToChange } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.unsubscribeNotification(accounts, id, hostname); + }); + + await waitForValueToChange(() => { + return result.current.isFetching; + }); + + expect(result.current.notifications.length).toBe(0); + }); + }); + }); + + describe('markRepoNotifications', () => { + const repoSlug = 'manosim/gitify'; + + describe('github.com', () => { + const accounts = { ...mockAccounts, enterpriseAccounts: [] }; + const hostname = 'github.com'; + + it("should mark a repository's notifications as read with success - github.com", async () => { + nock('https://api.github.com/') + .put(`/repos/${repoSlug}/notifications`) + .reply(200); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.markRepoNotifications(accounts, repoSlug, hostname); + }); + + await waitForNextUpdate(); + + expect(result.current.notifications.length).toBe(0); + }); + + it("should mark a repository's notifications as read with failure - github.com", async () => { + nock('https://api.github.com/') + .put(`/repos/${repoSlug}/notifications`) + .reply(400); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.markRepoNotifications(accounts, repoSlug, hostname); + }); + + await waitForNextUpdate(); + + expect(result.current.notifications.length).toBe(0); + }); + }); + + describe('enterprise', () => { + const accounts = { ...mockAccounts, token: null }; + const hostname = 'github.gitify.io'; + + it("should mark a repository's notifications as read with success - enterprise", async () => { + nock('https://github.gitify.io/') + .put(`/repos/${repoSlug}/notifications`) + .reply(200); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.markRepoNotifications(accounts, repoSlug, hostname); + }); + + await waitForNextUpdate(); + + expect(result.current.notifications.length).toBe(0); + }); + + it("should mark a repository's notifications as read with failure - enterprise", async () => { + nock('https://github.gitify.io/') + .put(`/repos/${repoSlug}/notifications`) + .reply(400); + + const { result, waitForNextUpdate } = renderHook(() => + useNotifications() + ); + + act(() => { + result.current.markRepoNotifications(accounts, repoSlug, hostname); + }); + + await waitForNextUpdate(); + + expect(result.current.notifications.length).toBe(0); + }); + }); + }); +}); diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts new file mode 100644 index 000000000..0e2b0f460 --- /dev/null +++ b/src/hooks/useNotifications.ts @@ -0,0 +1,214 @@ +import axios from 'axios'; +import { parse } from 'url'; +import { useCallback, useState } from 'react'; + +import { AccountNotifications, AuthState, SettingsState } from '../types'; +import { apiRequestAuth } from '../utils/api-requests'; +import { + getEnterpriseAccountToken, + generateGitHubAPIUrl, +} from '../utils/helpers'; +import { removeNotification } from '../utils/remove-notification'; +import { + triggerNativeNotifications, + setTrayIconColor, +} from '../utils/notifications'; +import Constants from '../utils/constants'; +import { removeNotifications } from '../utils/remove-notifications'; + +interface NotificationsState { + notifications: AccountNotifications[]; + fetchNotifications: ( + accounts: AuthState, + settings: SettingsState + ) => Promise; + markNotification: ( + accounts: AuthState, + id: string, + hostname: string + ) => Promise; + unsubscribeNotification: ( + accounts: AuthState, + id: string, + hostname: string + ) => Promise; + markRepoNotifications: ( + accounts: AuthState, + repoSlug: string, + hostname: string + ) => Promise; + isFetching: boolean; + requestFailed: boolean; +} + +export const useNotifications = (): NotificationsState => { + const [isFetching, setIsFetching] = useState(false); + const [requestFailed, setRequestFailed] = useState(false); + const [notifications, setNotifications] = useState( + [] + ); + + const fetchNotifications = useCallback( + async (accounts: AuthState, settings) => { + const isGitHubLoggedIn = accounts.token !== null; + const endpointSuffix = `notifications?participating=${settings.participating}`; + + function getGitHubNotifications() { + if (!isGitHubLoggedIn) { + return; + } + const url = `https://api.${Constants.DEFAULT_AUTH_OPTIONS.hostname}/${endpointSuffix}`; + return apiRequestAuth(url, 'GET', accounts.token); + } + + function getEnterpriseNotifications() { + return accounts.enterpriseAccounts.map((account) => { + const hostname = account.hostname; + const token = account.token; + const url = `https://${hostname}/api/v3/${endpointSuffix}`; + return apiRequestAuth(url, 'GET', token); + }); + } + + setIsFetching(true); + setRequestFailed(false); + + return axios + .all([getGitHubNotifications(), ...getEnterpriseNotifications()]) + .then( + axios.spread((gitHubNotifications, ...entAccNotifications) => { + const enterpriseNotifications = entAccNotifications.map( + (accountNotifications) => { + const { hostname } = parse(accountNotifications.config.url); + return { + hostname, + notifications: accountNotifications.data, + }; + } + ); + const data = isGitHubLoggedIn + ? [ + ...enterpriseNotifications, + { + hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname, + notifications: gitHubNotifications.data, + }, + ] + : [...enterpriseNotifications]; + + triggerNativeNotifications(notifications, data, settings); + setNotifications(data); + setIsFetching(false); + }) + ) + .catch(() => { + setIsFetching(false); + setRequestFailed(true); + }); + }, + [notifications] + ); + + const markNotification = useCallback( + async (accounts, id, hostname) => { + setIsFetching(true); + + const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; + const token = isEnterprise + ? getEnterpriseAccountToken(hostname, accounts.enterpriseAccounts) + : accounts.token; + + try { + await apiRequestAuth( + `${generateGitHubAPIUrl(hostname)}notifications/threads/${id}`, + 'PATCH', + token, + {} + ); + + const updatedNotifications = removeNotification( + id, + notifications, + hostname + ); + + setNotifications(updatedNotifications); + setTrayIconColor(updatedNotifications); + setIsFetching(false); + } catch (err) { + setIsFetching(false); + } + }, + [notifications] + ); + + const unsubscribeNotification = useCallback( + async (accounts, id, hostname) => { + setIsFetching(true); + + const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; + const token = isEnterprise + ? getEnterpriseAccountToken(hostname, accounts.enterpriseAccounts) + : accounts.token; + + try { + await apiRequestAuth( + `${generateGitHubAPIUrl( + hostname + )}notifications/threads/${id}/subscription`, + 'PUT', + token, + { ignore: true } + ); + await markNotification(accounts, id, hostname); + } catch (err) { + setIsFetching(false); + } + }, + [notifications] + ); + + const markRepoNotifications = useCallback( + async (accounts, repoSlug, hostname) => { + setIsFetching(true); + + const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; + const token = isEnterprise + ? getEnterpriseAccountToken(hostname, accounts.enterpriseAccounts) + : accounts.token; + + try { + await apiRequestAuth( + `${generateGitHubAPIUrl(hostname)}repos/${repoSlug}/notifications`, + 'PUT', + token, + {} + ); + + const updatedNotifications = removeNotifications( + repoSlug, + notifications, + hostname + ); + + setNotifications(updatedNotifications); + setTrayIconColor(updatedNotifications); + setIsFetching(false); + } catch (err) { + setIsFetching(false); + } + }, + [notifications] + ); + + return { + isFetching, + requestFailed, + notifications, + + fetchNotifications, + markNotification, + unsubscribeNotification, + markRepoNotifications, + }; +}; diff --git a/src/js/index.tsx b/src/index.tsx similarity index 100% rename from src/js/index.tsx rename to src/index.tsx diff --git a/src/js/__mocks__/redux-storage.js b/src/js/__mocks__/redux-storage.js deleted file mode 100644 index 1c7230be2..000000000 --- a/src/js/__mocks__/redux-storage.js +++ /dev/null @@ -1,21 +0,0 @@ -export function createMiddleware() { - return () => { - return (next) => (action) => { - return next(action); - }; - }; -} - -export function createLoader() { - const state = { - auth: { - token: 'LOGGED_IN_TOKEN', - }, - }; - - return () => new Promise((resolve) => resolve(state)); -} - -export function reducer(clb) { - return (state, action) => clb(state, action); -} diff --git a/src/js/actions/index.test.ts b/src/js/actions/index.test.ts deleted file mode 100644 index b9b560755..000000000 --- a/src/js/actions/index.test.ts +++ /dev/null @@ -1,718 +0,0 @@ -import axios from 'axios'; -import * as nock from 'nock'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; - -import { LOGOUT } from '../../types/actions'; -import { mockedEnterpriseAccounts } from '../__mocks__/mockedData'; -import * as actions from './'; -import Constants from '../utils/constants'; - -const middlewares = [thunk]; -const createMockStore = configureMockStore(middlewares); - -describe('actions/index.js', () => { - const mockEnterpriseAuthOptions = { - hostname: 'github.gitify.io', - clientId: '1a1a1a1a1a1a1a1a1a1a', - clientSecret: '2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b', - }; - - beforeEach(() => { - axios.defaults.adapter = require('axios/lib/adapters/http'); - }); - - afterEach(() => { - nock.cleanAll(); - }); - - it('should login a user (non enterprise) with success', () => { - const authOptions = Constants.DEFAULT_AUTH_OPTIONS; - const code = 'THISISACODE'; - - nock('https://github.com/').post('/login/oauth/access_token').reply(200, { - access_token: 'THISISATOKEN', - }); - - const expectedActions = [ - { type: actions.LOGIN.REQUEST }, - { - type: actions.LOGIN.SUCCESS, - isEnterprise: false, - hostname: 'github.com', - payload: { access_token: 'THISISATOKEN' }, - }, - ]; - - const store = createMockStore({}, expectedActions); - - return store.dispatch(actions.loginUser(authOptions, code)).then(() => { - // return of async actions - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should login a user (non enterprise) with failure', () => { - const authOptions = Constants.DEFAULT_AUTH_OPTIONS; - const code = 'THISISACODE'; - const message = 'Oops! Something went wrong.'; - - nock('https://github.com/') - .post('/login/oauth/access_token') - .reply(400, { message }); - - const expectedActions = [ - { type: actions.LOGIN.REQUEST }, - { - type: actions.LOGIN.FAILURE, - payload: { message }, - }, - ]; - - const store = createMockStore({}, expectedActions); - - return store.dispatch(actions.loginUser(authOptions, code)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should login a user (enterprise) with success', () => { - const authOptions = mockEnterpriseAuthOptions; - const code = 'THISISACODE'; - - nock('https://github.gitify.io/') - .post('/login/oauth/access_token') - .reply(200, { - access_token: 'THISISATOKEN', - }); - - const expectedActions = [ - { type: actions.LOGIN.REQUEST }, - { - type: actions.LOGIN.SUCCESS, - isEnterprise: true, - hostname: 'github.gitify.io', - payload: { access_token: 'THISISATOKEN' }, - }, - ]; - - const store = createMockStore({}, expectedActions); - - return store.dispatch(actions.loginUser(authOptions, code)).then(() => { - // return of async actions - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should login a user (enterprise) with failure', () => { - const authOptions = mockEnterpriseAuthOptions; - const code = 'THISISACODE'; - const message = 'Oops! Something went wrong.'; - - nock('https://github.gitify.io/') - .post('/login/oauth/access_token') - .reply(400, { message }); - - const expectedActions = [ - { type: actions.LOGIN.REQUEST }, - { - type: actions.LOGIN.FAILURE, - payload: { message }, - }, - ]; - - const store = createMockStore({}, expectedActions); - - return store.dispatch(actions.loginUser(authOptions, code)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should logout a user', () => { - const expectedAction = { - type: LOGOUT, - }; - - expect(actions.logout()).toEqual(expectedAction); - }); - - it('should fetch notifications with success - github.com & enterprise', () => { - const notifications = [ - { id: 1, title: 'This is a notification.' }, - { id: 2, title: 'This is another one.' }, - ]; - - nock('https://api.github.com/') - .get('/notifications?participating=false') - .reply(200, notifications); - - nock('https://github.gitify.io/api/v3/') - .get('/notifications?participating=false') - .reply(200, notifications); - - const expectedPayload = [ - { - hostname: 'github.gitify.io', - notifications: [ - { id: 1, title: 'This is a notification.' }, - { id: 2, title: 'This is another one.' }, - ], - }, - { - hostname: 'github.com', - notifications: [ - { id: 1, title: 'This is a notification.' }, - { id: 2, title: 'This is another one.' }, - ], - }, - ]; - - const expectedActions = [ - { type: actions.NOTIFICATIONS.REQUEST }, - { type: actions.NOTIFICATIONS.SUCCESS, payload: expectedPayload }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - settings: { - participating: false, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store.dispatch(actions.fetchNotifications()).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should fetch notifications with failure - github.com & enterprise', () => { - const message = 'Oops! Something went wrong.'; - - nock('https://api.github.com/') - .get('/notifications?participating=false') - .reply(400, { message }); - - nock('https://github.gitify.io/api/v3/') - .get('/notifications?participating=false') - .reply(400, { message }); - - const expectedActions = [ - { type: actions.NOTIFICATIONS.REQUEST }, - { type: actions.NOTIFICATIONS.FAILURE, payload: { message } }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - settings: { - participating: false, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store.dispatch(actions.fetchNotifications()).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should fetch notifications with success - enterprise only', () => { - const notifications = [ - { id: 1, title: 'This is a notification.' }, - { id: 2, title: 'This is another one.' }, - ]; - - nock('https://github.gitify.io/api/v3/') - .get('/notifications?participating=false') - .reply(200, notifications); - - const expectedPayload = [ - { - hostname: 'github.gitify.io', - notifications: [ - { id: 1, title: 'This is a notification.' }, - { id: 2, title: 'This is another one.' }, - ], - }, - ]; - - const expectedActions = [ - { type: actions.NOTIFICATIONS.REQUEST }, - { type: actions.NOTIFICATIONS.SUCCESS, payload: expectedPayload }, - ]; - - const store = createMockStore( - { - auth: { - token: null, - enterpriseAccounts: mockedEnterpriseAccounts, - }, - settings: { - participating: false, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store.dispatch(actions.fetchNotifications()).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should fetch notifications with failure - enterprise only', () => { - const message = 'Oops! Something went wrong.'; - - nock('https://github.gitify.io/api/v3/') - .get('/notifications?participating=false') - .reply(400, { message }); - - const expectedActions = [ - { type: actions.NOTIFICATIONS.REQUEST }, - { type: actions.NOTIFICATIONS.FAILURE, payload: { message } }, - ]; - - const store = createMockStore( - { - auth: { - token: null, - enterpriseAccounts: mockedEnterpriseAccounts, - }, - settings: { - participating: false, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store.dispatch(actions.fetchNotifications()).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should fetch notifications with success - github.com only', () => { - const notifications = [ - { id: 1, title: 'This is a notification.' }, - { id: 2, title: 'This is another one.' }, - ]; - - nock('https://api.github.com/') - .get('/notifications?participating=false') - .reply(200, notifications); - - const expectedPayload = [ - { - hostname: 'github.com', - notifications: notifications, - }, - ]; - - const expectedActions = [ - { type: actions.NOTIFICATIONS.REQUEST }, - { type: actions.NOTIFICATIONS.SUCCESS, payload: expectedPayload }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: [], - }, - settings: { - participating: false, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store.dispatch(actions.fetchNotifications()).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should fetch notifications with failure - github.com only', () => { - const message = 'Oops! Something went wrong.'; - - nock('https://api.github.com/') - .get('/notifications?participating=false') - .reply(400, { message }); - - const expectedActions = [ - { type: actions.NOTIFICATIONS.REQUEST }, - { type: actions.NOTIFICATIONS.FAILURE, payload: { message } }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: [], - }, - settings: { - participating: false, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store.dispatch(actions.fetchNotifications()).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should unsubscribe from a notification thread with success - github.com', () => { - const id = 123; - const hostname = 'github.com'; - - // The unsubscribe endpoint call. - nock('https://api.github.com/') - .put(`/notifications/threads/${id}/subscription`) - .reply(204); - - // The mark read endpoint call. - nock('https://api.github.com/') - .patch(`/notifications/threads/${id}`) - .reply(200); - - const expectedActions = [ - { type: actions.UNSUBSCRIBE_NOTIFICATION.REQUEST }, - { - type: actions.UNSUBSCRIBE_NOTIFICATION.SUCCESS, - meta: { id, hostname }, - }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store - .dispatch(actions.unsubscribeNotification(id, hostname)) - .then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should unsubscribe from a notification thread with failure - github.com', () => { - const id = 123; - const hostname = 'github.com'; - const message = 'Oops! Something went wrong.'; - - nock('https://api.github.com/') - .put(`/notifications/threads/${id}/subscription`) - .reply(400, { message }); - - const expectedActions = [ - { type: actions.UNSUBSCRIBE_NOTIFICATION.REQUEST }, - { type: actions.UNSUBSCRIBE_NOTIFICATION.FAILURE, payload: { message } }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - settings: { - participating: false, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store - .dispatch(actions.unsubscribeNotification(id, hostname)) - .then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should mark a notification as read with success - github.com', () => { - const id = 123; - const hostname = 'github.com'; - - nock('https://api.github.com/') - .patch(`/notifications/threads/${id}`) - .reply(200); - - const expectedActions = [ - { type: actions.MARK_NOTIFICATION.REQUEST }, - { - type: actions.MARK_NOTIFICATION.SUCCESS, - meta: { id, hostname }, - }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store.dispatch(actions.markNotification(id, hostname)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should mark a notification as read with failure - github.com', () => { - const id = 123; - const hostname = 'github.com'; - const message = 'Oops! Something went wrong.'; - - nock('https://api.github.com/') - .patch(`/notifications/threads/${id}`) - .reply(400, { message }); - - const expectedActions = [ - { type: actions.MARK_NOTIFICATION.REQUEST }, - { type: actions.MARK_NOTIFICATION.FAILURE, payload: { message } }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - settings: { - participating: false, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store.dispatch(actions.markNotification(id, hostname)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should mark a notification as read with success - enterprise', () => { - const id = 123; - const hostname = 'github.gitify.io'; - - nock('https://github.gitify.io/api/v3/') - .patch(`/notifications/threads/${id}`) - .reply(200, {}); - - const expectedActions = [ - { type: actions.MARK_NOTIFICATION.REQUEST }, - { - type: actions.MARK_NOTIFICATION.SUCCESS, - meta: { id, hostname }, - }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - notifications: { response: [] }, - }, - expectedActions - ); - - return store.dispatch(actions.markNotification(id, hostname)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should mark a notification as read with failure - enterprise', () => { - const id = 123; - const hostname = 'github.gitify.io'; - const message = 'Oops! Something went wrong.'; - - nock('https://github.gitify.io/api/v3/') - .patch(`/notifications/threads/${id}`) - .reply(400, { message }); - - const expectedActions = [ - { type: actions.MARK_NOTIFICATION.REQUEST }, - { type: actions.MARK_NOTIFICATION.FAILURE, payload: { message } }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - }, - expectedActions - ); - - return store.dispatch(actions.markNotification(id, hostname)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it("should mark a repository's notifications as read with success - github.com", () => { - const hostname = 'github.com'; - const repoSlug = 'manosim/gitify'; - const message = 'Success.'; - - nock('https://api.github.com/') - .put(`/repos/${repoSlug}/notifications`) - .reply(200, { message }); - - const expectedActions = [ - { type: actions.MARK_REPO_NOTIFICATION.REQUEST }, - { - type: actions.MARK_REPO_NOTIFICATION.SUCCESS, - payload: { message }, - meta: { repoSlug, hostname }, - }, - ]; - - const store = createMockStore( - { - auth: { token: 'IAMATOKEN' }, - }, - expectedActions - ); - - return store - .dispatch(actions.markRepoNotifications(repoSlug, hostname)) - .then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it("should mark a repository's notifications as read with failure - github.com", () => { - const hostname = 'github.com'; - const repoSlug = 'manosim/gitify'; - const message = 'Oops! Something went wrong.'; - - nock('https://api.github.com/') - .put(`/repos/${repoSlug}/notifications`) - .reply(400, { message }); - - const expectedActions = [ - { type: actions.MARK_REPO_NOTIFICATION.REQUEST }, - { type: actions.MARK_REPO_NOTIFICATION.FAILURE, payload: { message } }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - }, - expectedActions - ); - - return store - .dispatch(actions.markRepoNotifications(repoSlug, hostname)) - .then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it("should mark a repository's notifications as read with success - enterprise", () => { - const hostname = 'github.gitify.io'; - const repoSlug = 'manosim/gitify'; - const message = 'Success.'; - - nock('https://github.gitify.io/api/v3/') - .put(`/repos/${repoSlug}/notifications`) - .reply(200, { message }); - - const expectedActions = [ - { type: actions.MARK_REPO_NOTIFICATION.REQUEST }, - { - type: actions.MARK_REPO_NOTIFICATION.SUCCESS, - payload: { message }, - meta: { repoSlug, hostname }, - }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - }, - expectedActions - ); - - return store - .dispatch(actions.markRepoNotifications(repoSlug, hostname)) - .then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it("should mark a repository's notifications as read with failure - enterprise", () => { - const hostname = 'github.gitify.io'; - const repoSlug = 'manosim/gitify'; - const message = 'Oops! Something went wrong.'; - - nock('https://github.gitify.io/api/v3/') - .put(`/repos/${repoSlug}/notifications`) - .reply(400, { message }); - - const expectedActions = [ - { type: actions.MARK_REPO_NOTIFICATION.REQUEST }, - { type: actions.MARK_REPO_NOTIFICATION.FAILURE, payload: { message } }, - ]; - - const store = createMockStore( - { - auth: { - token: 'THISISATOKEN', - enterpriseAccounts: mockedEnterpriseAccounts, - }, - }, - expectedActions - ); - - return store - .dispatch(actions.markRepoNotifications(repoSlug, hostname)) - .then(() => { - expect(store.getActions()).toEqual(expectedActions); - }); - }); - - it('should update a setting for a user', () => { - const setting = 'participating'; - const value = true; - - const expectedAction = { - type: actions.UPDATE_SETTING, - setting, - value, - }; - - expect(actions.updateSetting(setting, value)).toEqual(expectedAction); - }); -}); diff --git a/src/js/actions/index.ts b/src/js/actions/index.ts deleted file mode 100644 index e64ac5f4b..000000000 --- a/src/js/actions/index.ts +++ /dev/null @@ -1,251 +0,0 @@ -import axios from 'axios'; -import { parse } from 'url'; - -import { apiRequest, apiRequestAuth } from '../utils/api-requests'; -import { - getEnterpriseAccountToken, - generateGitHubAPIUrl, -} from '../utils/helpers'; -import Constants from '../utils/constants'; -import { LogoutAction, LOGOUT } from '../../types/actions'; -import { SettingsState, AppState } from '../../types/reducers'; - -export function makeAsyncActionSet(actionName) { - return { - REQUEST: actionName + '_REQUEST', - SUCCESS: actionName + '_SUCCESS', - FAILURE: actionName + '_FAILURE', - }; -} - -enum Methods { - GET = 'GET', - POST = 'POST', - PUT = 'PUT', - PATCH = 'PATCH', - DELETE = 'DELETE', -} - -// Authentication - -export const LOGIN = makeAsyncActionSet('LOGIN'); -export function loginUser(authOptions, code) { - const { hostname } = authOptions; - const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; - - return (dispatch) => { - const url = `https://${hostname}/login/oauth/access_token`; - const data = { - client_id: authOptions.clientId, - client_secret: authOptions.clientSecret, - code: code, - }; - - dispatch({ type: LOGIN.REQUEST }); - - return apiRequest(url, Methods.POST, data) - .then(function (response) { - dispatch({ - type: LOGIN.SUCCESS, - payload: response.data, - isEnterprise, - hostname, - }); - }) - .catch(function (error) { - dispatch({ type: LOGIN.FAILURE, payload: error.response.data }); - }); - }; -} - -export function logout(): LogoutAction { - return { type: LOGOUT }; -} - -// Notifications - -export const NOTIFICATIONS = makeAsyncActionSet('NOTIFICATIONS'); -export function fetchNotifications() { - return (dispatch, getState: () => AppState) => { - const { settings }: { settings: SettingsState } = getState(); - const isGitHubLoggedIn = getState().auth.token !== null; - const endpointSuffix = `notifications?participating=${settings.participating}`; - - function getGitHubNotifications() { - if (!isGitHubLoggedIn) { - return; - } - - const url = `https://api.${Constants.DEFAULT_AUTH_OPTIONS.hostname}/${endpointSuffix}`; - const token = getState().auth.token; - return apiRequestAuth(url, Methods.GET, token); - } - - function getEnterpriseNotifications() { - const enterpriseAccounts = getState().auth.enterpriseAccounts; - return enterpriseAccounts.map((account) => { - const hostname = account.hostname; - const token = account.token; - const url = `https://${hostname}/api/v3/${endpointSuffix}`; - return apiRequestAuth(url, Methods.GET, token); - }); - } - - dispatch({ type: NOTIFICATIONS.REQUEST }); - - return axios - .all([getGitHubNotifications(), ...getEnterpriseNotifications()]) - .then( - axios.spread((gitHubNotifications, ...entAccNotifications) => { - const notifications = entAccNotifications.map( - (accountNotifications) => { - const { hostname } = parse(accountNotifications.config.url); - - return { - hostname, - notifications: accountNotifications.data, - }; - } - ); - - const allNotifications = isGitHubLoggedIn - ? [ - ...notifications, - { - hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname, - notifications: gitHubNotifications.data, - }, - ] - : [...notifications]; - - dispatch({ - type: NOTIFICATIONS.SUCCESS, - payload: allNotifications, - }); - }) - ) - .catch((error) => - dispatch({ type: NOTIFICATIONS.FAILURE, payload: error.response.data }) - ); - }; -} - -// Single Notification - -export const MARK_NOTIFICATION = makeAsyncActionSet('MARK_NOTIFICATION'); -export function markNotification(id, hostname) { - return (dispatch, getState: () => AppState) => { - const url = `${generateGitHubAPIUrl(hostname)}notifications/threads/${id}`; - - const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; - const entAccounts = getState().auth.enterpriseAccounts; - const token = isEnterprise - ? getEnterpriseAccountToken(hostname, entAccounts) - : getState().auth.token; - - dispatch({ type: MARK_NOTIFICATION.REQUEST }); - - return apiRequestAuth(url, Methods.PATCH, token, {}) - .then(function (response) { - dispatch({ - type: MARK_NOTIFICATION.SUCCESS, - meta: { id, hostname }, - }); - }) - .catch(function (error) { - dispatch({ - type: MARK_NOTIFICATION.FAILURE, - payload: error.response.data, - }); - }); - }; -} - -export const UNSUBSCRIBE_NOTIFICATION = makeAsyncActionSet( - 'UNSUBSCRIBE_NOTIFICATION' -); -export function unsubscribeNotification(id, hostname) { - return (dispatch, getState: () => AppState) => { - const markReadURL = `${generateGitHubAPIUrl( - hostname - )}notifications/threads/${id}`; - const unsubscribeURL = `${generateGitHubAPIUrl( - hostname - )}notifications/threads/${id}/subscription`; - - const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; - const entAccounts = getState().auth.enterpriseAccounts; - const token = isEnterprise - ? getEnterpriseAccountToken(hostname, entAccounts) - : getState().auth.token; - - dispatch({ type: UNSUBSCRIBE_NOTIFICATION.REQUEST }); - - return apiRequestAuth(unsubscribeURL, Methods.PUT, token, { ignore: true }) - .then((response) => { - // The GitHub notifications API doesn't automatically mark things as read - // like it does in the UI, so after unsubscribing we also need to hit the - // endpoint to mark it as read. - return apiRequestAuth(markReadURL, Methods.PATCH, token, {}); - }) - .then((response) => { - dispatch({ - type: UNSUBSCRIBE_NOTIFICATION.SUCCESS, - meta: { id, hostname }, - }); - }) - .catch((error) => { - dispatch({ - type: UNSUBSCRIBE_NOTIFICATION.FAILURE, - payload: error.response.data, - }); - }); - }; -} - -// Repo's Notification - -export const MARK_REPO_NOTIFICATION = makeAsyncActionSet( - 'MARK_REPO_NOTIFICATION' -); -export function markRepoNotifications(repoSlug, hostname) { - return (dispatch, getState: () => AppState) => { - const url = `${generateGitHubAPIUrl( - hostname - )}repos/${repoSlug}/notifications`; - - const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; - const entAccounts = getState().auth.enterpriseAccounts; - const token = isEnterprise - ? getEnterpriseAccountToken(hostname, entAccounts) - : getState().auth.token; - - dispatch({ type: MARK_REPO_NOTIFICATION.REQUEST }); - - return apiRequestAuth(url, Methods.PUT, token, {}) - .then(function (response) { - dispatch({ - type: MARK_REPO_NOTIFICATION.SUCCESS, - payload: response.data, - meta: { hostname, repoSlug }, - }); - }) - .catch(function (error) { - dispatch({ - type: MARK_REPO_NOTIFICATION.FAILURE, - payload: error.response.data, - }); - }); - }; -} - -// Settings - -export const UPDATE_SETTING = 'UPDATE_SETTING'; -export function updateSetting(setting, value) { - return { - type: UPDATE_SETTING, - setting: setting, - value: value, - }; -} diff --git a/src/js/components/__snapshots__/account-notifications.test.tsx.snap b/src/js/components/__snapshots__/account-notifications.test.tsx.snap deleted file mode 100644 index 4656c73a5..000000000 --- a/src/js/components/__snapshots__/account-notifications.test.tsx.snap +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/account-notifications.tsx should render itself (github.com with notifications) 1`] = ` -
- github.com -
-`; - -exports[`components/account-notifications.tsx should render itself (github.com without notifications) 1`] = ` -
- github.com -
-`; diff --git a/src/js/components/loading.test.tsx b/src/js/components/loading.test.tsx deleted file mode 100644 index c7ad8eda0..000000000 --- a/src/js/components/loading.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import * as NProgress from 'nprogress'; - -import { AppState } from '../../types/reducers'; -import { Loading, mapStateToProps } from './loading'; - -jest.mock('nprogress', () => { - return { - configure: jest.fn(), - start: jest.fn(), - done: jest.fn(), - remove: jest.fn(), - }; -}); - -describe('components/loading.js', function () { - beforeEach(() => { - NProgress.configure.mockReset(); - NProgress.start.mockReset(); - NProgress.done.mockReset(); - NProgress.remove.mockReset(); - }); - - it('should test the mapStateToProps method', () => { - const state = { - notifications: { - isFetching: false, - }, - } as AppState; - - const mappedProps = mapStateToProps(state); - - expect(mappedProps.isLoading).toBeFalsy(); - }); - - it('should check that NProgress is getting called in getDerivedStateFromProps (loading)', function () { - const { container } = render(); - - expect(container.innerHTML).toBe(''); - expect(NProgress.configure).toHaveBeenCalledTimes(1); - expect(NProgress.start).toHaveBeenCalledTimes(1); - }); - - it('should check that NProgress is getting called in getDerivedStateFromProps (not loading)', function () { - const { container } = render(); - - expect(container.innerHTML).toBe(''); - expect(NProgress.configure).toHaveBeenCalledTimes(1); - expect(NProgress.done).toHaveBeenCalledTimes(1); - }); - - it('should remove NProgress on unmount', function () { - const { unmount } = render(); - expect(NProgress.remove).toHaveBeenCalledTimes(0); - unmount(); - expect(NProgress.remove).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/js/components/loading.tsx b/src/js/components/loading.tsx deleted file mode 100644 index b377145c8..000000000 --- a/src/js/components/loading.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import * as NProgress from 'nprogress'; -import { AppState } from '../../types/reducers'; -import { connect } from 'react-redux'; - -export const Loading = ({ isLoading }: { isLoading: boolean }) => { - React.useEffect(() => { - NProgress.configure({ - showSpinner: false, - }); - - return () => { - NProgress.remove(); - }; - }, []); - - React.useEffect(() => { - if (isLoading) { - NProgress.start(); - } else { - NProgress.done(); - } - }, [isLoading]); - - return null; -}; - -export function mapStateToProps(state: AppState) { - return { - isLoading: state.notifications.isFetching, - }; -} - -export default connect(mapStateToProps, null)(Loading); diff --git a/src/js/components/notification.test.tsx b/src/js/components/notification.test.tsx deleted file mode 100644 index 164945823..000000000 --- a/src/js/components/notification.test.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import * as React from 'react'; -import * as TestRenderer from 'react-test-renderer'; -import { fireEvent, render } from '@testing-library/react'; - -const { shell } = require('electron'); - -import { mockedSingleNotification } from '../__mocks__/mockedData'; -import { NotificationItem, mapStateToProps } from './notification'; -import { SettingsState, AppState } from '../../types/reducers'; - -describe('components/notification.js', () => { - const notification = mockedSingleNotification; - - beforeEach(() => { - spyOn(shell, 'openExternal'); - }); - - it('should test the mapStateToProps method', () => { - const state = { - settings: { - markOnClick: true, - } as SettingsState, - } as AppState; - - const mappedProps = mapStateToProps(state); - - expect(mappedProps.markOnClick).toBeTruthy(); - }); - - it('should render itself & its children', async () => { - (global as any).Date.now = jest.fn(() => new Date('2014')); - - const props = { - markNotification: jest.fn(), - unsubscribeNotification: jest.fn(), - markOnClick: false, - notification: notification, - hostname: 'github.com', - }; - - const tree = TestRenderer.create(); - expect(tree).toMatchSnapshot(); - }); - - it('should open a notification in the browser', () => { - const props = { - markNotification: jest.fn(), - unsubscribeNotification: jest.fn(), - markOnClick: false, - notification: notification, - hostname: 'github.com', - }; - - const { getByRole } = render(); - fireEvent.click(getByRole('main')); - expect(shell.openExternal).toHaveBeenCalledTimes(1); - }); - - it('should open a notification in browser & mark it as read', () => { - const props = { - markNotification: jest.fn(), - unsubscribeNotification: jest.fn(), - markOnClick: true, - notification: notification, - hostname: 'github.com', - }; - - const { getByRole } = render(); - fireEvent.click(getByRole('main')); - expect(shell.openExternal).toHaveBeenCalledTimes(1); - expect(props.markNotification).toHaveBeenCalledTimes(1); - }); - - it('should mark a notification as read', () => { - const props = { - markNotification: jest.fn(), - unsubscribeNotification: jest.fn(), - markOnClick: false, - notification: notification, - hostname: 'github.com', - }; - - const { getByLabelText } = render(); - fireEvent.click(getByLabelText('Mark as Read')); - expect(props.markNotification).toHaveBeenCalledTimes(1); - }); - - it('should unsubscribe from a notification thread', () => { - const props = { - markNotification: jest.fn(), - unsubscribeNotification: jest.fn(), - markOnClick: false, - notification: notification, - hostname: 'github.com', - }; - - const { getByLabelText } = render(); - fireEvent.click(getByLabelText('Unsubscribe')); - expect(props.unsubscribeNotification).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/js/components/repository.test.tsx b/src/js/components/repository.test.tsx deleted file mode 100644 index ebbfb5bfb..000000000 --- a/src/js/components/repository.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from 'react'; -import * as TestRenderer from 'react-test-renderer'; -import { render, fireEvent } from '@testing-library/react'; - -import { mockedGithubNotifications } from '../__mocks__/mockedData'; -import { RepositoryNotifications } from './repository'; - -const { shell } = require('electron'); - -jest.mock('./notification'); - -describe('components/repository.tsx', function () { - const props = { - hostname: 'github.com', - repoName: 'manosim/gitify', - repoNotifications: mockedGithubNotifications, - markRepoNotifications: jest.fn(), - }; - - beforeEach(() => { - spyOn(shell, 'openExternal'); - }); - - it('should render itself & its children', function () { - const tree = TestRenderer.create(); - expect(tree).toMatchSnapshot(); - }); - - it('should open the browser when clicking on the repo name', function () { - const { getByText } = render(); - - fireEvent.click(getByText(props.repoName)); - - expect(shell.openExternal).toHaveBeenCalledTimes(1); - expect(shell.openExternal).toHaveBeenCalledWith( - 'https://github.com/manosim/notifications-test' - ); - }); - - it('should mark a repo as read', function () { - const { getByRole, debug } = render(); - - fireEvent.click(getByRole('button')); - - expect(props.markRepoNotifications).toHaveBeenCalledWith( - 'manosim/notifications-test', - 'github.com' - ); - }); -}); diff --git a/src/js/components/sidebar.test.tsx b/src/js/components/sidebar.test.tsx deleted file mode 100644 index eec589100..000000000 --- a/src/js/components/sidebar.test.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import * as React from 'react'; -import * as TestRenderer from 'react-test-renderer'; -import { render, fireEvent } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; - -const { shell, ipcRenderer } = require('electron'); - -import { Sidebar, mapStateToProps } from './sidebar'; -import { mockedEnterpriseAccounts } from '../__mocks__/mockedData'; -import { AuthState, AppState } from '../../types/reducers'; - -describe('components/Sidebar.tsx', () => { - let clock; - - const props = { - isEitherLoggedIn: true, - connectedAccounts: 2, - notificationsCount: 4, - fetchNotifications: jest.fn(), - history: { - goBack: jest.fn(), - push: jest.fn(), - }, - location: { - pathname: '/', - }, - }; - - beforeEach(() => { - clock = jest.useFakeTimers(); - - spyOn(ipcRenderer, 'send'); - spyOn(shell, 'openExternal'); - spyOn(window, 'clearInterval'); - - props.fetchNotifications.mockReset(); - props.history.goBack.mockReset(); - props.history.push.mockReset(); - }); - - afterEach(() => { - clock.clearAllTimers(); - }); - - describe('mapStateToProps', () => { - const state = { - auth: { - token: '12345', - enterpriseAccounts: mockedEnterpriseAccounts, - } as AuthState, - notifications: { - response: [{ hostname: 'Dolores', notifications: [{}, {}] }], - }, - } as AppState; - - it('should accept a provided token', () => { - const mappedProps = mapStateToProps(state); - expect(mappedProps.isEitherLoggedIn).toBeTruthy(); - expect(mappedProps.connectedAccounts).toBe(2); - }); - - it('should count notification lengths', () => { - const mappedProps = mapStateToProps(state); - expect(mappedProps.notificationsCount).toBe(2); - }); - - it('should accept a null token', () => { - const mappedProps = mapStateToProps({ - ...state, - auth: { - ...state.auth, - token: null, - }, - }); - expect(mappedProps.isEitherLoggedIn).toBeTruthy(); - expect(mappedProps.connectedAccounts).toBe(1); - }); - }); - - it('should render itself & its children (logged in)', () => { - const tree = TestRenderer.create( - - - - ); - expect(tree).toMatchSnapshot(); - }); - - it('should render itself & its children (logged out)', function () { - const caseProps = { - ...props, - notifications: [], - enterpriseAccounts: [], - isGitHubLoggedIn: false, - isEitherLoggedIn: false, - }; - const tree = TestRenderer.create( - - - - ); - expect(tree).toMatchSnapshot(); - }); - - it('should clear the interval when unmounting', () => { - spyOn(Sidebar.prototype, 'componentDidMount').and.callThrough(); - - const { unmount } = render( - - - - ); - - expect(Sidebar.prototype.componentDidMount).toHaveBeenCalledTimes(1); - unmount(); - expect(window.clearInterval).toHaveBeenCalledTimes(1); - }); - - it('should load notifications after 60000ms', function () { - const {} = render( - - - - ); - - expect(props.fetchNotifications).toHaveBeenCalledTimes(1); - clock.runTimersToTime(60000); - expect(props.fetchNotifications).toHaveBeenCalledTimes(2); - clock.runTimersToTime(60000); - expect(props.fetchNotifications).toHaveBeenCalledTimes(3); - }); - - it('should fetch the notifications if another account logs in', () => { - const { rerender } = render( - - - - ); - - props.fetchNotifications.mockReset(); - - rerender( - - - - ); - - expect(props.fetchNotifications).toHaveBeenCalledTimes(1); - }); - - it('should refresh the notifications', () => { - const { getByLabelText } = render( - - - - ); - props.fetchNotifications.mockReset(); - fireEvent.click(getByLabelText('Refresh Notifications')); - expect(props.fetchNotifications).toHaveBeenCalledTimes(1); - }); - - it('go to the settings route', () => { - const { getByLabelText } = render( - - - - ); - fireEvent.click(getByLabelText('Settings')); - expect(props.history.push).toHaveBeenCalledTimes(1); - }); - - it('go to back to home from the settings route', () => { - const caseProps = { - ...props, - location: { - pathname: '/settings', - }, - }; - const { getByLabelText } = render( - - - - ); - fireEvent.click(getByLabelText('Settings')); - expect(props.history.goBack).toHaveBeenCalledTimes(1); - }); - - it('open the gitify repo in browser', () => { - const { getByLabelText } = render( - - - - ); - fireEvent.click(getByLabelText('View project on GitHub')); - expect(shell.openExternal).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/js/components/sidebar.tsx b/src/js/components/sidebar.tsx deleted file mode 100644 index 0a5ce0b65..000000000 --- a/src/js/components/sidebar.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import * as React from 'react'; -import { compose } from 'redux'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; -import { shell } from 'electron'; -import * as Octicons from '@primer/octicons-react'; - -import { AppState } from '../../types/reducers'; -import { fetchNotifications, logout } from '../actions'; -import { isUserEitherLoggedIn } from '../utils/helpers'; -import { Logo } from './ui/logo'; -import Constants from '../utils/constants'; - -interface IProps { - fetchNotifications: () => void; - - connectedAccounts: number; - notificationsCount: number; - isEitherLoggedIn: boolean; - - history: any; - location: any; -} - -export class Sidebar extends React.Component { - requestInterval: any; - - componentDidMount() { - const self = this; - const iFrequency = 60000; - - this.requestInterval = setInterval(() => { - self.refreshNotifications(); - }, iFrequency); - } - - state = { - connectedAccounts: [], - }; - - static getDerivedStateFromProps(props, state) { - if (props.connectedAccounts > state.connectedAccounts) { - props.fetchNotifications(); - } - - return { - connectedAccounts: props.connectedAccounts, - }; - } - - componentWillUnmount() { - clearInterval(this.requestInterval); - } - - refreshNotifications() { - if (this.props.isEitherLoggedIn) { - this.props.fetchNotifications(); - } - } - - onOpenBrowser() { - shell.openExternal(`https://github.com/${Constants.REPO_SLUG}`); - } - - goToSettings() { - if (this.props.location.pathname === '/settings') { - return this.props.history.goBack(); - } - return this.props.history.push('/settings'); - } - - render() { - const { isEitherLoggedIn, notificationsCount } = this.props; - - const footerButtonClasses = - 'flex justify-evenly items-center bg-transparent border-0 w-full text-sm text-white my-1 py-2 cursor-pointer hover:text-gray-500 focus:outline-none'; - - return ( -
-
- - - {notificationsCount > 0 && ( -
- - {notificationsCount} -
- )} -
- -
- {isEitherLoggedIn && ( - <> - - - - - )} - -
- -
-
-
- ); - } -} - -export function mapStateToProps(state: AppState) { - const enterpriseAccounts = state.auth.enterpriseAccounts; - const isGitHubLoggedIn = state.auth.token !== null; - const connectedAccounts = isGitHubLoggedIn - ? enterpriseAccounts.length + 1 - : enterpriseAccounts.length; - - const notificationsCount = state.notifications.response.reduce( - (memo, account) => memo + account.notifications.length, - 0 - ); - - return { - isEitherLoggedIn: isUserEitherLoggedIn(state.auth), - notificationsCount, - connectedAccounts, - }; -} - -export default compose( - withRouter, - connect(mapStateToProps, { - fetchNotifications, - logout, - }) -)(Sidebar) as any; diff --git a/src/js/middleware/notifications.test.ts b/src/js/middleware/notifications.test.ts deleted file mode 100644 index 751eb6884..000000000 --- a/src/js/middleware/notifications.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import * as actions from '../actions'; -import * as comms from '../utils/comms'; -import { - mockedGithubNotifications, - mockedNotificationsReducerData, -} from '../__mocks__/mockedData'; -import notificationsMiddleware from './notifications'; -import NativeNotifications from '../utils/notifications'; - -// Keep 3 notifications -// Ps. To receive 4 on actions.NOTIFICATIONS.SUCCESS, -const mockedNotifications = mockedNotificationsReducerData.map( - (account, accountIndex) => { - if (accountIndex === 0) { - return { - ...account, - notifications: account.notifications.filter((_, index) => index !== 0), - }; - } - - return account; - } -); - -const DEFAULT_STORE = { - notifications: { - response: mockedNotifications, - }, - settings: { - playSound: false, - showNotifications: false, - }, -}; - -const createFakeStore = (storeData) => ({ - getState() { - return storeData; - }, -}); - -const dispatchWithStoreOf = (storeData, action) => { - let dispatched = null; - const dispatch = notificationsMiddleware( - createFakeStore({ ...DEFAULT_STORE, ...storeData }) - )((actionAttempt) => (dispatched = actionAttempt)); - dispatch(action); - return dispatched; -}; - -describe('middleware/notifications.js', () => { - beforeEach(() => { - spyOn(NativeNotifications, 'setup').and.stub(); - spyOn(comms, 'updateTrayIcon').and.stub(); - }); - - it('should raise notifications (native & sound, update tray icon, set badge)', () => { - const action = { - type: actions.NOTIFICATIONS.SUCCESS, - payload: mockedNotificationsReducerData, - }; - - expect(dispatchWithStoreOf({}, action)).toEqual(action); - - const newNotifications = [[mockedGithubNotifications[0]], []]; - - expect(NativeNotifications.setup).toHaveBeenCalledTimes(1); - expect(NativeNotifications.setup).toHaveBeenCalledWith( - newNotifications, - 1, - { playSound: false, showNotifications: false } - ); - }); - - it('should mark a notification and call the update tray icon helper', () => { - const action = { - type: actions.MARK_NOTIFICATION.SUCCESS, - }; - - expect(dispatchWithStoreOf({}, action)).toEqual(action); - - expect(comms.updateTrayIcon).toHaveBeenCalledTimes(1); - expect(comms.updateTrayIcon).toHaveBeenCalledWith(2); - }); - - it("should mark a repo's notification and call the update tray icon helper", () => { - const action = { - type: actions.MARK_REPO_NOTIFICATION.SUCCESS, - meta: { - repoSlug: 'manosim/notifications-test', - hostname: 'github.com', - }, - }; - - expect(dispatchWithStoreOf({}, action)).toEqual(action); - expect(comms.updateTrayIcon).toHaveBeenCalledTimes(1); - expect(comms.updateTrayIcon).toHaveBeenCalledWith(2); - }); - - it('should update tray icon with no notifications', () => { - const noNewNotifications = mockedNotificationsReducerData.map((host) => ({ - ...host, - notifications: [], - })); - const action = { - type: actions.NOTIFICATIONS.SUCCESS, - payload: noNewNotifications, - }; - dispatchWithStoreOf( - { - ...DEFAULT_STORE, - notifications: { - response: noNewNotifications, - }, - }, - action - ); - expect(comms.updateTrayIcon).toHaveBeenCalledTimes(1); - expect(comms.updateTrayIcon).toHaveBeenCalledWith(0); - }); - - it('should show 0 notifications if no accounts logged in', () => { - const action = { - type: actions.NOTIFICATIONS.SUCCESS, - payload: mockedNotificationsReducerData, - }; - dispatchWithStoreOf( - { - ...DEFAULT_STORE, - notifications: { - response: [], - }, - }, - action - ); - expect(comms.updateTrayIcon).toHaveBeenCalledTimes(1); - expect(comms.updateTrayIcon).toHaveBeenCalledWith(4); - }); -}); diff --git a/src/js/middleware/notifications.ts b/src/js/middleware/notifications.ts deleted file mode 100644 index 835f4e438..000000000 --- a/src/js/middleware/notifications.ts +++ /dev/null @@ -1,93 +0,0 @@ -import * as _ from 'lodash'; -import { - NOTIFICATIONS, - MARK_NOTIFICATION, - MARK_REPO_NOTIFICATION, - UNSUBSCRIBE_NOTIFICATION, -} from '../actions'; -import NativeNotifications from '../utils/notifications'; -import { updateTrayIcon } from '../utils/comms'; -import { AccountNotifications } from '../../types/reducers'; - -export default (store) => (next) => (action) => { - const settings = store.getState().settings; - const accountNotifications: AccountNotifications[] = store.getState() - .notifications.response; - - switch (action.type) { - case NOTIFICATIONS.SUCCESS: - const previousNotifications = accountNotifications.map((account) => { - return { - hostname: account.hostname, - notifications: account.notifications.map( - (notification) => notification.id - ), - }; - }); - - const newNotifications = action.payload.map((AccountNotifications) => { - const accountPreviousNotifications = - previousNotifications.length > 0 - ? previousNotifications.find( - (obj) => obj.hostname === AccountNotifications.hostname - ).notifications - : []; - - return AccountNotifications.notifications.filter((obj) => { - return !accountPreviousNotifications.includes(obj.id); - }); - }); - - const newNotificationsCount = newNotifications.reduce( - (memo, acc) => memo + acc.length, - 0 - ); - - const allNotificationsCount = action.payload.reduce( - (memo, acc) => memo + acc.notifications.length, - 0 - ); - - updateTrayIcon(allNotificationsCount); - NativeNotifications.setup( - newNotifications, - newNotificationsCount, - settings - ); - break; - - case MARK_NOTIFICATION.SUCCESS: - case UNSUBSCRIBE_NOTIFICATION.SUCCESS: - const prevNotificationsCount = accountNotifications.reduce( - (memo, acc) => memo + acc.notifications.length, - 0 - ); - - updateTrayIcon(prevNotificationsCount - 1); - break; - - case MARK_REPO_NOTIFICATION.SUCCESS: - const updatedNotificationsCount = accountNotifications - .map((accNotifications) => { - if (accNotifications.hostname !== action.meta.hostname) { - return accNotifications; - } - - return _.updateWith( - accNotifications, - '[notifications]', - (notifications) => { - return notifications.filter( - (obj) => obj.repository.full_name !== action.meta.repoSlug - ); - } - ); - }) - .reduce((memo, acc) => memo + acc.notifications.length, 0); - - updateTrayIcon(updatedNotificationsCount); - break; - } - - return next(action); -}; diff --git a/src/js/middleware/settings.test.ts b/src/js/middleware/settings.test.ts deleted file mode 100644 index ba9714d2e..000000000 --- a/src/js/middleware/settings.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -const { remote } = require('electron'); - -import * as actions from '../actions'; -import settingsMiddleware from './settings'; - -const dispatchWithStoreOf = (_, action) => { - let dispatched = null; - const dispatch = settingsMiddleware()( - (actionAttempt) => (dispatched = actionAttempt) - ); - dispatch(action); - return dispatched; -}; - -describe('middleware/settings.js', () => { - it('should toggle the openAtStartup setting', () => { - spyOn(remote.app, 'setLoginItemSettings'); - - const action = { - type: actions.UPDATE_SETTING, - setting: 'openAtStartup', - value: true, - }; - - expect(dispatchWithStoreOf({}, action)).toEqual(action); - - expect(remote.app.setLoginItemSettings).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/js/middleware/settings.ts b/src/js/middleware/settings.ts deleted file mode 100644 index b0fdc03a4..000000000 --- a/src/js/middleware/settings.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { UPDATE_SETTING } from '../actions'; -import { setAppearance } from '../utils/appearance'; -import { setAutoLaunch } from '../utils/comms'; - -export default () => (next) => (action) => { - switch (action.type) { - case UPDATE_SETTING: - if (action.setting === 'openAtStartup') { - setAutoLaunch(action.value); - } - - if (action.setting === 'appearance') { - setAppearance(action.value); - } - } - - return next(action); -}; diff --git a/src/js/reducers/__snapshots__/auth.test.ts.snap b/src/js/reducers/__snapshots__/auth.test.ts.snap deleted file mode 100644 index a7e1374b4..000000000 --- a/src/js/reducers/__snapshots__/auth.test.ts.snap +++ /dev/null @@ -1,67 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`reducers/auth.ts should handle LOGIN.FAILURE 1`] = ` -Object { - "failed": true, - "isFetching": false, - "response": Object { - "msg": "Failed to login.", - }, - "token": null, -} -`; - -exports[`reducers/auth.ts should handle LOGIN.REQUEST 1`] = ` -Object { - "enterpriseAccounts": Array [], - "failed": false, - "isFetching": true, - "response": Object {}, - "token": null, -} -`; - -exports[`reducers/auth.ts should handle LOGIN.SUCCESS - enterprise 1`] = ` -Object { - "enterpriseAccounts": Array [ - Object { - "hostname": "github.gitify.io", - "token": "123HELLOWORLDTOKEN", - }, - ], - "failed": false, - "isFetching": false, - "response": Object {}, - "token": null, -} -`; - -exports[`reducers/auth.ts should handle LOGIN.SUCCESS - github.com 1`] = ` -Object { - "enterpriseAccounts": Array [], - "failed": false, - "isFetching": false, - "response": Object {}, - "token": "123HELLOWORLDTOKEN", -} -`; - -exports[`reducers/auth.ts should handle LOGOUT 1`] = ` -Object { - "enterpriseAccounts": Array [], - "failed": false, - "isFetching": false, - "response": null, - "token": null, -} -`; - -exports[`reducers/auth.ts should return the initial state 1`] = ` -Object { - "enterpriseAccounts": Array [], - "failed": false, - "isFetching": false, - "response": Object {}, - "token": null, -} -`; diff --git a/src/js/reducers/__snapshots__/notifications.test.ts.snap b/src/js/reducers/__snapshots__/notifications.test.ts.snap deleted file mode 100644 index 29792b080..000000000 --- a/src/js/reducers/__snapshots__/notifications.test.ts.snap +++ /dev/null @@ -1,315 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`reducers/notifications.ts should handle LOGOUT 1`] = ` -Object { - "failed": false, - "isFetching": false, - "response": Array [], -} -`; - -exports[`reducers/notifications.ts should handle MARK_NOTIFICATION.SUCCESS - enterprise 1`] = ` -Object { - "failed": false, - "isFetching": false, - "response": Array [ - Object { - "hostname": "github.gitify.io", - "notifications": Array [ - Object { - "id": "3", - "last_read_at": "2017-05-20T14:20:55Z", - "reason": "subscribed", - "repository": Object { - "description": null, - "fork": false, - "full_name": "myorg/notifications-test", - "html_url": "https://github.gitify.io/myorg/notifications-test", - "id": 1, - "name": "notifications-test", - "owner": Object { - "avatar_url": "https://github.gitify.io/avatars/u/4?", - "events_url": "https://github.gitify.io/api/v3/users/myorg/events{/privacy}", - "followers_url": "https://github.gitify.io/api/v3/users/myorg/followers", - "following_url": "https://github.gitify.io/api/v3/users/myorg/following{/other_user}", - "gists_url": "https://github.gitify.io/api/v3/users/myorg/gists{/gist_id}", - "gravatar_id": "", - "html_url": "https://github.gitify.io/myorg", - "id": 4, - "login": "myorg", - "organizations_url": "https://github.gitify.io/api/v3/users/myorg/orgs", - "received_events_url": "https://github.gitify.io/api/v3/users/myorg/received_events", - "repos_url": "https://github.gitify.io/api/v3/users/myorg/repos", - "site_admin": false, - "starred_url": "https://github.gitify.io/api/v3/users/myorg/starred{/owner}{/repo}", - "subscriptions_url": "https://github.gitify.io/api/v3/users/myorg/subscriptions", - "type": "Organization", - "url": "https://github.gitify.io/api/v3/users/myorg", - }, - "private": true, - }, - "subject": Object { - "latest_comment_url": "https://github.gitify.io/api/v3/repos/myorg/notifications-test/issues/comments/21", - "title": "Bump Version", - "type": "PullRequest", - "url": "https://github.gitify.io/api/v3/repos/myorg/notifications-test/pulls/3", - }, - "subscription_url": "https://github.gitify.io/api/v3/notifications/threads/3/subscription", - "unread": true, - "updated_at": "2017-05-20T15:52:20Z", - "url": "https://github.gitify.io/api/v3/notifications/threads/3", - }, - ], - }, - ], -} -`; - -exports[`reducers/notifications.ts should handle MARK_NOTIFICATION.SUCCESS - github.com 1`] = ` -Object { - "failed": false, - "isFetching": false, - "response": Array [ - Object { - "hostname": "github.com", - "notifications": Array [ - Object { - "id": "148827438", - "last_read_at": "2017-05-20T16:59:03Z", - "reason": "author", - "repository": Object { - "description": null, - "fork": false, - "full_name": "manosim/notifications-test", - "html_url": "https://github.com/manosim/notifications-test", - "id": 57216596, - "name": "notifications-test", - "owner": Object { - "avatar_url": "https://avatars0.githubusercontent.com/u/6333409?v=3", - "events_url": "https://api.github.com/users/manosim/events{/privacy}", - "followers_url": "https://api.github.com/users/manosim/followers", - "following_url": "https://api.github.com/users/manosim/following{/other_user}", - "gists_url": "https://api.github.com/users/manosim/gists{/gist_id}", - "gravatar_id": "", - "html_url": "https://github.com/manosim", - "id": 6333409, - "login": "manosim", - "organizations_url": "https://api.github.com/users/manosim/orgs", - "received_events_url": "https://api.github.com/users/manosim/received_events", - "repos_url": "https://api.github.com/users/manosim/repos", - "site_admin": false, - "starred_url": "https://api.github.com/users/manosim/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/manosim/subscriptions", - "type": "User", - "url": "https://api.github.com/users/manosim", - }, - "private": true, - }, - "subject": Object { - "latest_comment_url": "https://api.github.com/repos/manosim/notifications-test/issues/comments/302885965", - "title": "Improve the UI", - "type": "Issue", - "url": "https://api.github.com/repos/manosim/notifications-test/issues/4", - }, - "subscription_url": "https://api.github.com/notifications/threads/148827438/subscription", - "unread": true, - "updated_at": "2017-05-20T17:06:34Z", - "url": "https://api.github.com/notifications/threads/148827438", - }, - ], - }, - ], -} -`; - -exports[`reducers/notifications.ts should handle MARK_REPO_NOTIFICATION.SUCCESS - enterprise 1`] = ` -Object { - "failed": false, - "isFetching": false, - "response": Array [ - Object { - "hostname": "github.gitify.io", - "notifications": Array [], - }, - ], -} -`; - -exports[`reducers/notifications.ts should handle MARK_REPO_NOTIFICATION.SUCCESS - github.com 1`] = ` -Object { - "failed": false, - "isFetching": false, - "response": Array [ - Object { - "hostname": "github.gitify.io", - "notifications": Array [], - }, - ], -} -`; - -exports[`reducers/notifications.ts should handle NOTIFICATIONS.FAILURE 1`] = ` -Object { - "failed": true, - "isFetching": false, - "response": Array [], -} -`; - -exports[`reducers/notifications.ts should handle NOTIFICATIONS.REQUEST 1`] = ` -Object { - "failed": false, - "isFetching": true, - "response": Array [], -} -`; - -exports[`reducers/notifications.ts should handle NOTIFICATIONS.SUCCESS 1`] = ` -Object { - "failed": false, - "isFetching": false, - "response": Array [], -} -`; - -exports[`reducers/notifications.ts should handle NOTIFICATIONS.SUCCESS 2`] = ` -Object { - "failed": false, - "isFetching": false, - "response": Array [ - Object { - "id": "138661096", - "last_read_at": "2017-05-20T17:06:51Z", - "reason": "subscribed", - "repository": Object { - "archive_url": "https://api.github.com/repos/manosim/notifications-test/{archive_format}{/ref}", - "assignees_url": "https://api.github.com/repos/manosim/notifications-test/assignees{/user}", - "blobs_url": "https://api.github.com/repos/manosim/notifications-test/git/blobs{/sha}", - "branches_url": "https://api.github.com/repos/manosim/notifications-test/branches{/branch}", - "collaborators_url": "https://api.github.com/repos/manosim/notifications-test/collaborators{/collaborator}", - "comments_url": "https://api.github.com/repos/manosim/notifications-test/comments{/number}", - "commits_url": "https://api.github.com/repos/manosim/notifications-test/commits{/sha}", - "compare_url": "https://api.github.com/repos/manosim/notifications-test/compare/{base}...{head}", - "contents_url": "https://api.github.com/repos/manosim/notifications-test/contents/{+path}", - "contributors_url": "https://api.github.com/repos/manosim/notifications-test/contributors", - "deployments_url": "https://api.github.com/repos/manosim/notifications-test/deployments", - "description": "Test Repository", - "downloads_url": "https://api.github.com/repos/manosim/notifications-test/downloads", - "events_url": "https://api.github.com/repos/manosim/notifications-test/events", - "fork": false, - "forks_url": "https://api.github.com/repos/manosim/notifications-test/forks", - "full_name": "manosim/notifications-test", - "git_commits_url": "https://api.github.com/repos/manosim/notifications-test/git/commits{/sha}", - "git_refs_url": "https://api.github.com/repos/manosim/notifications-test/git/refs{/sha}", - "git_tags_url": "https://api.github.com/repos/manosim/notifications-test/git/tags{/sha}", - "hooks_url": "https://api.github.com/repos/manosim/notifications-test/hooks", - "html_url": "https://github.com/manosim/notifications-test", - "id": 57216596, - "issue_comment_url": "https://api.github.com/repos/manosim/notifications-test/issues/comments{/number}", - "issue_events_url": "https://api.github.com/repos/manosim/notifications-test/issues/events{/number}", - "issues_url": "https://api.github.com/repos/manosim/notifications-test/issues{/number}", - "keys_url": "https://api.github.com/repos/manosim/notifications-test/keys{/key_id}", - "labels_url": "https://api.github.com/repos/manosim/notifications-test/labels{/name}", - "languages_url": "https://api.github.com/repos/manosim/notifications-test/languages", - "merges_url": "https://api.github.com/repos/manosim/notifications-test/merges", - "milestones_url": "https://api.github.com/repos/manosim/notifications-test/milestones{/number}", - "name": "notifications-test", - "node_id": "MDEwOlJlcG9zaXRvcnkzNjAyOTcwNg==", - "notifications_url": "https://api.github.com/repos/manosim/notifications-test/notifications{?since,all,participating}", - "owner": Object { - "avatar_url": "https://avatars0.githubusercontent.com/u/6333409?v=3", - "events_url": "https://api.github.com/users/manosim/events{/privacy}", - "followers_url": "https://api.github.com/users/manosim/followers", - "following_url": "https://api.github.com/users/manosim/following{/other_user}", - "gists_url": "https://api.github.com/users/manosim/gists{/gist_id}", - "gravatar_id": "", - "html_url": "https://github.com/manosim", - "id": 6333409, - "login": "manosim", - "node_id": "MDQ6VXNlcjYzMzM0MDk=", - "organizations_url": "https://api.github.com/users/manosim/orgs", - "received_events_url": "https://api.github.com/users/manosim/received_events", - "repos_url": "https://api.github.com/users/manosim/repos", - "site_admin": false, - "starred_url": "https://api.github.com/users/manosim/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/manosim/subscriptions", - "type": "User", - "url": "https://api.github.com/users/manosim", - }, - "private": true, - "pulls_url": "https://api.github.com/repos/manosim/notifications-test/pulls{/number}", - "releases_url": "https://api.github.com/repos/manosim/notifications-test/releases{/id}", - "stargazers_url": "https://api.github.com/repos/manosim/notifications-test/stargazers", - "statuses_url": "https://api.github.com/repos/manosim/notifications-test/statuses/{sha}", - "subscribers_url": "https://api.github.com/repos/manosim/notifications-test/subscribers", - "subscription_url": "https://api.github.com/repos/manosim/notifications-test/subscription", - "tags_url": "https://api.github.com/repos/manosim/notifications-test/tags", - "teams_url": "https://api.github.com/repos/manosim/notifications-test/teams", - "trees_url": "https://api.github.com/repos/manosim/notifications-test/git/trees{/sha}", - "url": "https://api.github.com/manosim/notifications-test", - }, - "subject": Object { - "latest_comment_url": "https://api.github.com/repos/manosim/notifications-test/issues/comments/302888448", - "title": "I am a robot and this is a test!", - "type": "Issue", - "url": "https://api.github.com/repos/manosim/notifications-test/issues/1", - }, - "subscription_url": "https://api.github.com/notifications/threads/138661096/subscription", - "unread": true, - "updated_at": "2017-05-20T17:51:57Z", - "url": "https://api.github.com/notifications/threads/138661096", - }, - Object { - "id": "148827438", - "last_read_at": "2017-05-20T16:59:03Z", - "reason": "author", - "repository": Object { - "description": null, - "fork": false, - "full_name": "manosim/notifications-test", - "html_url": "https://github.com/manosim/notifications-test", - "id": 57216596, - "name": "notifications-test", - "owner": Object { - "avatar_url": "https://avatars0.githubusercontent.com/u/6333409?v=3", - "events_url": "https://api.github.com/users/manosim/events{/privacy}", - "followers_url": "https://api.github.com/users/manosim/followers", - "following_url": "https://api.github.com/users/manosim/following{/other_user}", - "gists_url": "https://api.github.com/users/manosim/gists{/gist_id}", - "gravatar_id": "", - "html_url": "https://github.com/manosim", - "id": 6333409, - "login": "manosim", - "organizations_url": "https://api.github.com/users/manosim/orgs", - "received_events_url": "https://api.github.com/users/manosim/received_events", - "repos_url": "https://api.github.com/users/manosim/repos", - "site_admin": false, - "starred_url": "https://api.github.com/users/manosim/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/manosim/subscriptions", - "type": "User", - "url": "https://api.github.com/users/manosim", - }, - "private": true, - }, - "subject": Object { - "latest_comment_url": "https://api.github.com/repos/manosim/notifications-test/issues/comments/302885965", - "title": "Improve the UI", - "type": "Issue", - "url": "https://api.github.com/repos/manosim/notifications-test/issues/4", - }, - "subscription_url": "https://api.github.com/notifications/threads/148827438/subscription", - "unread": true, - "updated_at": "2017-05-20T17:06:34Z", - "url": "https://api.github.com/notifications/threads/148827438", - }, - ], -} -`; - -exports[`reducers/notifications.ts should return the initial state 1`] = ` -Object { - "failed": false, - "isFetching": false, - "response": Array [], -} -`; diff --git a/src/js/reducers/__snapshots__/settings.test.ts.snap b/src/js/reducers/__snapshots__/settings.test.ts.snap deleted file mode 100644 index 38feec562..000000000 --- a/src/js/reducers/__snapshots__/settings.test.ts.snap +++ /dev/null @@ -1,34 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`reducers/settings.ts should handle UPDATE_SETTING 1`] = ` -Object { - "appearance": "SYSTEM", - "markOnClick": false, - "openAtStartup": false, - "participating": true, - "playSound": true, - "showNotifications": true, -} -`; - -exports[`reducers/settings.ts should handle UPDATE_SETTING 2`] = ` -Object { - "appearance": "SYSTEM", - "markOnClick": false, - "openAtStartup": true, - "participating": false, - "playSound": true, - "showNotifications": true, -} -`; - -exports[`reducers/settings.ts should return the initial state 1`] = ` -Object { - "appearance": "SYSTEM", - "markOnClick": false, - "openAtStartup": false, - "participating": false, - "playSound": true, - "showNotifications": true, -} -`; diff --git a/src/js/reducers/auth.test.ts b/src/js/reducers/auth.test.ts deleted file mode 100644 index e9f36e5ae..000000000 --- a/src/js/reducers/auth.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { AuthState } from '../../types/reducers'; -import { LOGIN } from '../actions'; -import { LOGOUT } from '../../types/actions'; -import reducer from './auth'; - -describe('reducers/auth.ts', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toMatchSnapshot(); - }); - - it('should handle LOGIN.REQUEST', () => { - const action = { - type: LOGIN.REQUEST, - }; - - expect(reducer(undefined, action)).toMatchSnapshot(); - }); - - it('should handle LOGIN.SUCCESS - github.com', () => { - const fakeState = { - response: {}, - token: null, - isFetching: true, - failed: false, - } as AuthState; - - expect(reducer(fakeState, {}).token).toBeNull(); - - const action = { - type: LOGIN.SUCCESS, - payload: { - access_token: '123HELLOWORLDTOKEN', - }, - }; - - expect(reducer(undefined, action)).toMatchSnapshot(); - expect(reducer(fakeState, action).token).toBe('123HELLOWORLDTOKEN'); - }); - - it('should handle LOGIN.SUCCESS - enterprise', () => { - const action = { - type: LOGIN.SUCCESS, - isEnterprise: true, - hostname: 'github.gitify.io', - payload: { - access_token: '123HELLOWORLDTOKEN', - }, - }; - - expect(reducer(undefined, action)).toMatchSnapshot(); - }); - - it('should handle LOGIN.FAILURE', () => { - const fakeState = { - response: {}, - token: null, - isFetching: true, - failed: false, - } as AuthState; - - expect(reducer(fakeState, {}).token).toBeNull(); - - const action = { - type: LOGIN.FAILURE, - payload: { msg: 'Failed to login.' }, - }; - - expect(reducer(fakeState, action)).toMatchSnapshot(); - expect(reducer(fakeState, action).token).toBeNull(); - }); - - it('should handle LOGOUT', () => { - const fakeState = { - response: {}, - token: 'LOGGEDINTOKEN', - isFetching: false, - failed: false, - enterpriseAccounts: [], - } as AuthState; - - expect(reducer(fakeState, {}).token).not.toBeNull(); - - const action = { - type: LOGOUT, - }; - - expect(reducer(fakeState, action)).toMatchSnapshot(); - expect(reducer(fakeState, action).token).toBeNull(); - }); -}); diff --git a/src/js/reducers/auth.ts b/src/js/reducers/auth.ts deleted file mode 100644 index 96a94edb0..000000000 --- a/src/js/reducers/auth.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { LOGIN } from '../actions'; -import { LOGOUT } from '../../types/actions'; -import { AuthState, EnterpriseAccount } from '../../types/reducers'; - -const initialState: AuthState = { - response: {}, - token: null, - isFetching: false, - failed: false, - enterpriseAccounts: [], -}; - -export default function reducer(state = initialState, action): AuthState { - switch (action.type) { - case LOGIN.REQUEST: - return { - ...state, - isFetching: true, - failed: false, - response: {}, - }; - case LOGIN.SUCCESS: - if (action.isEnterprise) { - const enterpriseAccount: EnterpriseAccount = { - hostname: action.hostname, - token: action.payload.access_token, - }; - return { - ...state, - isFetching: false, - enterpriseAccounts: [...state.enterpriseAccounts, enterpriseAccount], - }; - } - - return { - ...state, - isFetching: false, - token: action.payload.access_token, - }; - case LOGIN.FAILURE: - return { - ...state, - isFetching: false, - failed: true, - response: action.payload, - }; - case LOGOUT: - return { - ...state, - response: null, - token: null, - enterpriseAccounts: [], - }; - default: - return state; - } -} diff --git a/src/js/reducers/index.js b/src/js/reducers/index.js deleted file mode 100644 index 6e3911f31..000000000 --- a/src/js/reducers/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import { combineReducers } from 'redux'; -import * as storage from 'redux-storage'; - -import auth from './auth'; -import notifications from './notifications'; -import settings from './settings'; - -export default storage.reducer( - combineReducers({ - auth, - notifications, - settings, - }) -); diff --git a/src/js/reducers/notifications.test.ts b/src/js/reducers/notifications.test.ts deleted file mode 100644 index 53c1ebd1a..000000000 --- a/src/js/reducers/notifications.test.ts +++ /dev/null @@ -1,258 +0,0 @@ -import Constants from '../utils/constants'; -import reducer from './notifications'; -import { - NOTIFICATIONS, - MARK_NOTIFICATION, - MARK_REPO_NOTIFICATION, -} from '../actions'; -import { - mockedGithubNotifications, - mockedEnterpriseNotifications, -} from '../__mocks__/mockedData'; -import { LOGOUT } from '../../types/actions'; -import { NotificationsState } from '../../types/reducers'; - -describe('reducers/notifications.ts', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toMatchSnapshot(); - }); - - it('should handle NOTIFICATIONS.REQUEST', () => { - const action = { - type: NOTIFICATIONS.REQUEST, - }; - - expect(reducer(undefined, action)).toMatchSnapshot(); - }); - - it('should handle NOTIFICATIONS.SUCCESS', () => { - expect(reducer(undefined, {})).toMatchSnapshot(); - - const action = { - type: NOTIFICATIONS.SUCCESS, - payload: mockedGithubNotifications, - hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname, - }; - - expect(reducer(undefined, action)).toMatchSnapshot(); - }); - - it('should handle NOTIFICATIONS.FAILURE', () => { - const action = { - type: NOTIFICATIONS.FAILURE, - }; - - expect(reducer(undefined, action)).toMatchSnapshot(); - }); - - it('should handle MARK_NOTIFICATION.SUCCESS - github.com', () => { - const id = '138661096'; - const hostname = 'github.com'; - - const action = { - type: MARK_NOTIFICATION.SUCCESS, - meta: { id, hostname }, - }; - - const currentState: NotificationsState = { - isFetching: false, - failed: false, - response: [ - { - hostname, - notifications: mockedGithubNotifications, - }, - ], - }; - - expect(reducer(currentState, action)).toMatchSnapshot(); - }); - - it('should handle MARK_NOTIFICATION.SUCCESS - github.com (without snapshot)', () => { - const id = '138661096'; - const hostname = 'github.com'; - - const action = { - type: MARK_NOTIFICATION.SUCCESS, - meta: { id, hostname }, - }; - - const currentState: NotificationsState = { - isFetching: false, - failed: false, - response: [ - { - hostname, - notifications: mockedGithubNotifications, - }, - ], - }; - - expect(reducer(currentState, action)).toMatchObject({ - isFetching: false, - failed: false, - response: [ - { - hostname, - notifications: [mockedGithubNotifications[1]], - }, - ], - }); - }); - - it('should handle MARK_NOTIFICATION.SUCCESS - enterprise', () => { - const id = '4'; - const hostname = 'github.gitify.io'; - - const action = { - type: MARK_NOTIFICATION.SUCCESS, - meta: { id, hostname }, - }; - - const currentState: NotificationsState = { - isFetching: false, - failed: false, - response: [ - { - hostname, - notifications: mockedEnterpriseNotifications, - }, - ], - }; - - expect(reducer(currentState, action)).toMatchSnapshot(); - }); - - it('should handle MARK_NOTIFICATION.SUCCESS - enterprise (without snapshot)', () => { - const id = '4'; - const hostname = 'github.gitify.io'; - - const action = { - type: MARK_NOTIFICATION.SUCCESS, - meta: { id, hostname }, - }; - - const currentState: NotificationsState = { - isFetching: false, - failed: false, - response: [ - { - hostname, - notifications: mockedEnterpriseNotifications, - }, - ], - }; - - expect(reducer(currentState, action)).toMatchObject({ - isFetching: false, - failed: false, - response: [ - { - hostname, - notifications: mockedEnterpriseNotifications.filter( - (item) => item.id !== id - ), - }, - ], - }); - }); - - it('should handle MARK_REPO_NOTIFICATION.SUCCESS - github.com', () => { - const repoSlug = 'manosim/notifications-test'; - const hostname = 'github.gitify.io'; - - const currentState: NotificationsState = { - isFetching: false, - failed: false, - response: [ - { - hostname, - notifications: mockedGithubNotifications, - }, - ], - }; - - const action = { - type: MARK_REPO_NOTIFICATION.SUCCESS, - meta: { hostname, repoSlug }, - }; - - expect(reducer(currentState, action)).toMatchSnapshot(); - }); - - it('should handle MARK_REPO_NOTIFICATION.SUCCESS - github.com (without snapshot)', () => { - const repoSlug = 'manosim/notifications-test'; - const hostname = 'github.com'; - - const currentState: NotificationsState = { - isFetching: false, - failed: false, - response: [ - { - hostname, - notifications: mockedGithubNotifications, - }, - ], - }; - - const action = { - type: MARK_REPO_NOTIFICATION.SUCCESS, - meta: { hostname, repoSlug }, - }; - - expect(reducer(currentState, action)).toMatchObject({ - isFetching: false, - failed: false, - response: [ - { - hostname, - notifications: mockedGithubNotifications.filter( - (item) => item.repository.full_name !== repoSlug - ), - }, - ], - }); - }); - - it('should handle MARK_REPO_NOTIFICATION.SUCCESS - enterprise', () => { - const repoSlug = 'myorg/notifications-test'; - const hostname = 'github.gitify.io'; - - const currentState: NotificationsState = { - isFetching: false, - failed: false, - response: [ - { - hostname, - notifications: mockedEnterpriseNotifications, - }, - ], - }; - - const action = { - type: MARK_REPO_NOTIFICATION.SUCCESS, - meta: { hostname, repoSlug }, - }; - - expect(reducer(currentState, action)).toMatchSnapshot(); - }); - - it('should handle LOGOUT', () => { - const currentState: NotificationsState = { - isFetching: false, - failed: false, - response: [ - { - hostname: 'github.gitify.io', - notifications: mockedEnterpriseNotifications, - }, - ], - }; - - const action = { - type: LOGOUT, - }; - - expect(reducer(currentState, action)).toMatchSnapshot(); - }); -}); diff --git a/src/js/reducers/notifications.ts b/src/js/reducers/notifications.ts deleted file mode 100644 index 8c566411b..000000000 --- a/src/js/reducers/notifications.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as _ from 'lodash'; -import { - NOTIFICATIONS, - MARK_NOTIFICATION, - MARK_REPO_NOTIFICATION, - UNSUBSCRIBE_NOTIFICATION, -} from '../actions'; -import { LOGOUT } from '../../types/actions'; -import { Notification } from '../../types/github'; -import { NotificationsState } from '../../types/reducers'; - -const initialState: NotificationsState = { - response: [], - isFetching: false, - failed: false, -}; - -export default function reducer( - state = initialState, - action -): NotificationsState { - switch (action.type) { - case NOTIFICATIONS.REQUEST: - return { ...state, isFetching: true, failed: false }; - case NOTIFICATIONS.SUCCESS: - return { ...state, isFetching: false, response: action.payload }; - case NOTIFICATIONS.FAILURE: - return { ...state, isFetching: false, failed: true, response: [] }; - case MARK_NOTIFICATION.SUCCESS: - case UNSUBSCRIBE_NOTIFICATION.SUCCESS: - const accountIndex = state.response.findIndex( - (obj) => obj.hostname === action.meta.hostname - ); - - return _.updateWith( - { ...state }, - `[response][${accountIndex}][notifications]`, - (accNotifications: Notification[]) => { - return accNotifications.filter( - (notification) => notification.id !== action.meta.id - ); - } - ); - case MARK_REPO_NOTIFICATION.SUCCESS: - const accNotificationsRepoIndex = state.response.findIndex( - (obj) => obj.hostname === action.meta.hostname - ); - - return _.updateWith( - { ...state }, - `[response][${accNotificationsRepoIndex}][notifications]`, - (accNotifications) => { - return accNotifications.filter( - (notification) => - notification.repository.full_name !== action.meta.repoSlug - ); - } - ); - case LOGOUT: - return initialState; - default: - return state; - } -} diff --git a/src/js/reducers/settings.test.ts b/src/js/reducers/settings.test.ts deleted file mode 100644 index bd188e243..000000000 --- a/src/js/reducers/settings.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import reducer from './settings'; -import { UPDATE_SETTING } from '../actions'; - -describe('reducers/settings.ts', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toMatchSnapshot(); - }); - - it('should handle UPDATE_SETTING', () => { - const actionParticipating = { - type: UPDATE_SETTING, - setting: 'participating', - value: true, - }; - - expect(reducer(undefined, actionParticipating)).toMatchSnapshot(); - - const actionOpenAtStartUp = { - type: UPDATE_SETTING, - setting: 'openAtStartup', - value: true, - }; - - expect(reducer(undefined, actionOpenAtStartUp)).toMatchSnapshot(); - }); -}); diff --git a/src/js/reducers/settings.ts b/src/js/reducers/settings.ts deleted file mode 100644 index 474d0ef77..000000000 --- a/src/js/reducers/settings.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { UPDATE_SETTING } from '../actions'; -import { SettingsState } from '../../types/reducers'; -import { Appearance } from '../../types'; - -const initialState: SettingsState = { - participating: false, - playSound: true, - showNotifications: true, - markOnClick: false, - openAtStartup: false, - appearance: Appearance.SYSTEM, -}; - -export default function reducer(state = initialState, action): SettingsState { - switch (action.type) { - case UPDATE_SETTING: - return { ...state, [action.setting]: action.value }; - default: - return state; - } -} diff --git a/src/js/routes/enterprise-login.test.tsx b/src/js/routes/enterprise-login.test.tsx deleted file mode 100644 index af0207624..000000000 --- a/src/js/routes/enterprise-login.test.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import * as React from 'react'; -import * as TestRenderer from 'react-test-renderer'; -import { fireEvent, render } from '@testing-library/react'; -import { MemoryRouter } from 'react-router'; -import { Provider } from 'react-redux'; -import { createStore } from 'redux'; - -import { mockedEnterpriseAccounts } from '../__mocks__/mockedData'; - -const { ipcRenderer, remote } = require('electron'); -const BrowserWindow = remote.BrowserWindow; -const dialog = remote.dialog; - -import { EnterpriseLogin, mapStateToProps, validate } from './enterprise-login'; -import { AppState } from '../../types/reducers'; - -describe('routes/enterprise-login.js', () => { - const props = { - enterpriseAccountsCount: 0, - dispatch: jest.fn(), - handleSubmit: (cb) => cb, - history: { - goBack: jest.fn(), - }, - }; - - beforeEach(function () { - // @ts-ignore - new BrowserWindow().loadURL.mockReset(); - spyOn(ipcRenderer, 'send'); - props.dispatch.mockReset(); - props.history.goBack = jest.fn(); - }); - - it('should test the mapStateToProps method', () => { - const state = { - auth: { - enterpriseAccounts: mockedEnterpriseAccounts, - }, - } as AppState; - - const mappedProps = mapStateToProps(state); - - expect(mappedProps.enterpriseAccountsCount).toBe( - mockedEnterpriseAccounts.length - ); - }); - - it('renders correctly', () => { - const tree = TestRenderer.create( - {})}> - - - - - ); - - expect(tree).toMatchSnapshot(); - }); - - it('let us go back', () => { - props.history.goBack = jest.fn(); - const { getByLabelText } = render( - {})}> - - - - - ); - fireEvent.click(getByLabelText('Go Back')); - expect(props.history.goBack).toHaveBeenCalledTimes(1); - }); - - it('should validate the form values', () => { - let values; - const emptyValues = { - hostname: null, - clientId: null, - clientSecret: null, - }; - - values = { - ...emptyValues, - }; - expect(validate(values).hostname).toBe('Required'); - expect(validate(values).clientId).toBe('Required'); - expect(validate(values).clientSecret).toBe('Required'); - - values = { - ...emptyValues, - hostname: 'hello', - clientId: '!@£INVALID-.1', - clientSecret: '!@£INVALID-.1', - }; - expect(validate(values).hostname).toBe('Invalid hostname.'); - expect(validate(values).clientId).toBe('Invalid client id.'); - expect(validate(values).clientSecret).toBe('Invalid client secret.'); - }); - - it('should receive a logged-in enterprise account', () => { - const caseProps = { - ...props, - }; - - const { rerender } = render( - {})}> - - - - - ); - - rerender( - {})}> - - - - - ); - - expect(ipcRenderer.send).toHaveBeenCalledTimes(1); - expect(ipcRenderer.send).toHaveBeenCalledWith('reopen-window'); - expect(props.history.goBack).toHaveBeenCalledTimes(1); - }); - - it('should render the form with errors', () => { - const { getByLabelText, getByTitle, getByText } = render( - {})}> - - - - - ); - - fireEvent.change(getByLabelText('Hostname'), { - target: { value: 'test' }, - }); - fireEvent.change(getByLabelText('Client ID'), { - target: { value: '123' }, - }); - fireEvent.change(getByLabelText('Client Secret'), { - target: { value: 'abc' }, - }); - - fireEvent.submit(getByTitle('Login Button')); - - expect(getByText('Invalid hostname.')).toBeTruthy(); - expect(getByText('Invalid client id.')).toBeTruthy(); - expect(getByText('Invalid client secret.')).toBeTruthy(); - }); - - it('should open the login window and get a code successfully (will-redirect)', () => { - const code = '123123123'; - const hostname = 'github.gitify.io'; - const clientId = '12312312312312312312'; - - spyOn(new BrowserWindow().webContents, 'on').and.callFake( - (event, callback) => { - if (event === 'will-redirect') { - const event = new Event('will-redirect'); - callback(event, `https://${hostname}/?code=${code}`); - } - } - ); - - const expectedUrl = `https://${hostname}/login/oauth/authorize?client_id=${clientId}&scope=user:email,notifications`; - - const { getByLabelText, getByTitle } = render( - {})}> - - - - - ); - - fireEvent.change(getByLabelText('Hostname'), { - target: { value: hostname }, - }); - fireEvent.change(getByLabelText('Client ID'), { - target: { value: clientId }, - }); - fireEvent.change(getByLabelText('Client Secret'), { - target: { value: 'ABC123ABCDABC123ABCDABC123ABCDABC123ABCD' }, - }); - - fireEvent.submit(getByTitle('Login Button')); - - expect(new BrowserWindow().loadURL).toHaveBeenCalledTimes(1); - expect(new BrowserWindow().loadURL).toHaveBeenCalledWith(expectedUrl); - expect(props.dispatch).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/js/routes/enterprise-login.tsx b/src/js/routes/enterprise-login.tsx deleted file mode 100644 index 805de5d57..000000000 --- a/src/js/routes/enterprise-login.tsx +++ /dev/null @@ -1,155 +0,0 @@ -const ipcRenderer = require('electron').ipcRenderer; - -import * as React from 'react'; -import { Form, FormRenderProps } from 'react-final-form'; -import { connect } from 'react-redux'; -import { ArrowLeftIcon } from '@primer/octicons-react'; - -import { AppState } from '../../types/reducers'; -import { authGithub } from '../utils/helpers'; -import { FieldInput } from '../components/fields/input'; - -interface IValues { - hostname?: string; - clientId?: string; - clientSecret?: string; -} - -interface IFormErrors { - hostname?: string; - clientId?: string; - clientSecret?: string; -} - -export const validate = (values: IValues): IFormErrors => { - const errors: IFormErrors = {}; - if (!values.hostname) { - errors.hostname = 'Required'; - } else if ( - !/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$/i.test( - values.hostname - ) - ) { - errors.hostname = 'Invalid hostname.'; - } - - if (!values.clientId) { - // 20 - errors.clientId = 'Required'; - } else if (!/^[A-Z0-9]{20}$/i.test(values.clientId)) { - errors.clientId = 'Invalid client id.'; - } - - if (!values.clientSecret) { - // 40 - errors.clientSecret = 'Required'; - } else if (!/^[A-Z0-9]{40}$/i.test(values.clientSecret)) { - errors.clientSecret = 'Invalid client secret.'; - } - - return errors; -}; - -interface IProps { - history: any; - dispatch: any; - enterpriseAccountsCount: number; -} - -interface IState { - enterpriseAccountsCount: number; -} - -export class EnterpriseLogin extends React.Component { - state = { - enterpriseAccountsCount: 0, - }; - - static getDerivedStateFromProps(props: IProps, state) { - if (props.enterpriseAccountsCount > state.enterpriseAccountsCount) { - ipcRenderer.send('reopen-window'); - props.history.goBack(); - } - - return { - enterpriseAccountsCount: props.enterpriseAccountsCount, - }; - } - - renderForm = (formProps: FormRenderProps) => { - const { handleSubmit, submitting, pristine } = formProps; - - return ( -
- - - - - - - - - ); - }; - - handleSubmit(data, dispatch) { - authGithub(data, dispatch); - } - - render() { - return ( -
-
- - -

- Login with GitHub Enterprise -

-
- -
-
this.handleSubmit(data, this.props.dispatch)} - validate={validate} - > - {this.renderForm} -
-
-
- ); - } -} - -export function mapStateToProps(state: AppState) { - return { - enterpriseAccountsCount: state.auth.enterpriseAccounts.length, - }; -} - -export default connect(mapStateToProps, null)(EnterpriseLogin); diff --git a/src/js/routes/login.test.tsx b/src/js/routes/login.test.tsx deleted file mode 100644 index 59c5d58ce..000000000 --- a/src/js/routes/login.test.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import * as React from 'react'; -import * as TestRenderer from 'react-test-renderer'; -import { MemoryRouter } from 'react-router'; -import { render, fireEvent, getByLabelText } from '@testing-library/react'; - -import { mockedEnterpriseAccounts } from '../__mocks__/mockedData'; - -const { ipcRenderer, remote } = require('electron'); -const BrowserWindow = remote.BrowserWindow; - -import { AppState } from '../../types/reducers'; -import { LoginPage, mapStateToProps } from './login'; -import * as helpers from '../utils/helpers'; - -describe('routes/login.tsx', () => { - const props = { - dispatch: jest.fn(), - isEitherLoggedIn: false, - history: { - goBack: jest.fn(), - push: jest.fn(), - }, - }; - - beforeEach(function () { - // @ts-ignore - new BrowserWindow().loadURL.mockReset(); - spyOn(ipcRenderer, 'send'); - props.dispatch.mockReset(); - props.history.push.mockReset(); - }); - - it('should test the mapStateToProps method', () => { - const state = { - auth: { - token: '123456', - failed: false, - isFetching: false, - enterpriseAccounts: mockedEnterpriseAccounts, - }, - } as AppState; - - const mappedProps = mapStateToProps(state); - - expect(mappedProps.isEitherLoggedIn).toBeTruthy(); - }); - - it('should render itself & its children', () => { - const caseProps = { - ...props, - }; - - const tree = TestRenderer.create( - - - - ); - - expect(tree).toMatchSnapshot(); - }); - - it('should redirect to notifications once logged in', () => { - const caseProps = { - ...props, - }; - - const { rerender } = render( - - - - ); - - rerender( - - - - ); - - expect(ipcRenderer.send).toHaveBeenCalledTimes(1); - expect(ipcRenderer.send).toHaveBeenCalledWith('reopen-window'); - }); - - it('should call the authGitHub helper when pressing the login button', () => { - spyOn(helpers, 'authGithub'); - const caseProps = { - ...props, - }; - - const { getByLabelText } = render( - - - - ); - - fireEvent.click(getByLabelText('Login with GitHub')); - - expect(helpers.authGithub).toHaveBeenCalledTimes(1); - }); - - it('should navigate to login with github enterprise', () => { - const { getByLabelText } = render( - - - - ); - - fireEvent.click(getByLabelText('Login with GitHub Enterprise')); - - expect(props.history.push).toHaveBeenCalledTimes(1); - expect(props.history.push).toHaveBeenCalledWith('/enterpriselogin'); - }); -}); diff --git a/src/js/routes/login.tsx b/src/js/routes/login.tsx deleted file mode 100644 index bc791bb2b..000000000 --- a/src/js/routes/login.tsx +++ /dev/null @@ -1,77 +0,0 @@ -const { ipcRenderer } = require('electron'); - -import * as React from 'react'; -import { Redirect } from 'react-router-dom'; -import { connect } from 'react-redux'; - -import { AppState } from '../../types/reducers'; -import { authGithub, isUserEitherLoggedIn } from '../utils/helpers'; -import { Logo } from '../components/ui/logo'; - -interface IProps { - isEitherLoggedIn: boolean; - dispatch: any; - history: any; -} - -export class LoginPage extends React.Component { - constructor(props: IProps) { - super(props); - - this.state = { - isEitherLoggedIn: props.isEitherLoggedIn, - }; - } - - static getDerivedStateFromProps(props, state) { - if (props.isEitherLoggedIn) { - ipcRenderer.send('reopen-window'); - } - return { - isEitherLoggedIn: props.isEitherLoggedIn, - }; - } - - render() { - if (this.props.isEitherLoggedIn) { - return ; - } - - const loginButtonClass = - 'w-48 py-2 my-2 bg-gray-300 font-semibold rounded text-xs text-center dark:text-black hover:bg-gray-500 hover:text-white focus:outline-none'; - - return ( -
- - -
- GitHub Notifications
on your menu bar. -
- - - - -
- ); - } -} - -export function mapStateToProps(state: AppState) { - return { - isEitherLoggedIn: isUserEitherLoggedIn(state.auth), - }; -} - -export default connect(mapStateToProps)(LoginPage); diff --git a/src/js/routes/notifications.test.tsx b/src/js/routes/notifications.test.tsx deleted file mode 100644 index 2f5464990..000000000 --- a/src/js/routes/notifications.test.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from 'react'; -import * as TestRenderer from 'react-test-renderer'; - -import { AppState, NotificationsState } from '../../types/reducers'; -import { mockedNotificationsReducerData } from '../__mocks__/mockedData'; -import { NotificationsRoute, mapStateToProps } from './notifications'; - -jest.mock('../components/account-notifications', () => ({ - AccountNotifications: 'AccountNotifications', -})); - -jest.mock('../components/all-read', () => ({ - AllRead: 'AllRead', -})); - -jest.mock('../components/oops', () => ({ - Oops: 'Oops', -})); - -describe('routes/notifications.ts', () => { - const props = { - failed: false, - accountNotifications: mockedNotificationsReducerData, - notificationsCount: 4, - hasMultipleAccounts: true, - hasNotifications: true, - }; - - it('should test the mapStateToProps method', () => { - const state = { - notifications: { - response: mockedNotificationsReducerData, - failed: false, - } as NotificationsState, - } as AppState; - - const mappedProps = mapStateToProps(state); - - expect(mappedProps.failed).toBeFalsy(); - expect(mappedProps.accountNotifications).toEqual( - mockedNotificationsReducerData - ); - expect(mappedProps.hasNotifications).toBeTruthy(); - expect(mappedProps.notificationsCount).toBe(4); - }); - - it('should render itself & its children (with notifications)', () => { - const tree = TestRenderer.create(); - - expect(tree).toMatchSnapshot(); - }); - - it('should render itself & its children (all read notifications)', () => { - const caseProps = { - ...props, - hasNotifications: false, - accountNotifications: [], - }; - - const tree = TestRenderer.create(); - - expect(tree).toMatchSnapshot(); - }); - - it('should render itself & its children (error page - oops)', () => { - const caseProps = { - ...props, - failed: true, - }; - - const tree = TestRenderer.create(); - - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/src/js/routes/notifications.tsx b/src/js/routes/notifications.tsx deleted file mode 100644 index bba8ab88d..000000000 --- a/src/js/routes/notifications.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react'; -import { connect } from 'react-redux'; - -import { AccountNotifications as Account } from '../components/account-notifications'; -import { AllRead } from '../components/all-read'; -import { AppState, AccountNotifications } from '../../types/reducers'; -import { Oops } from '../components/oops'; - -interface IProps { - hasMultipleAccounts: boolean; - hasNotifications: boolean; - accountNotifications: AccountNotifications[]; - notificationsCount: number; - failed: boolean; -} - -export const NotificationsRoute = (props: IProps) => { - const { accountNotifications, hasMultipleAccounts, hasNotifications } = props; - - if (props.failed) { - return ; - } - - if (!hasNotifications) { - return ; - } - - return ( -
- {accountNotifications.map((account) => ( - - ))} -
- ); -}; - -export function mapStateToProps(state: AppState) { - const notificationsCount = state.notifications.response.reduce( - (memo, acc) => memo + acc.notifications.length, - 0 - ); - const hasMultipleAccounts = state.notifications.response.length > 1; - - return { - failed: state.notifications.failed, - accountNotifications: state.notifications.response, - notificationsCount, - hasMultipleAccounts, - hasNotifications: notificationsCount > 0, - }; -} - -export default connect(mapStateToProps, null)(NotificationsRoute); diff --git a/src/js/routes/settings.test.tsx b/src/js/routes/settings.test.tsx deleted file mode 100644 index 38eccfb3c..000000000 --- a/src/js/routes/settings.test.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import * as React from 'react'; -import * as TestRenderer from 'react-test-renderer'; -import { render, fireEvent } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; - -const { ipcRenderer } = require('electron'); - -import { SettingsRoute, mapStateToProps } from './settings'; -import { - SettingsState, - AppState, - AuthState, - EnterpriseAccount, -} from '../../types/reducers'; - -describe('routes/settings.tsx', () => { - const props = { - updateSetting: jest.fn(), - fetchNotifications: jest.fn(), - hasMultipleAccounts: false, - logout: jest.fn(), - history: { - push: jest.fn(), - goBack: jest.fn(), - replace: jest.fn(), - }, - settings: { - participating: false, - playSound: true, - showNotifications: true, - markOnClick: false, - openAtStartup: false, - } as SettingsState, - }; - - beforeEach(function () { - spyOn(ipcRenderer, 'send'); - - props.updateSetting.mockReset(); - props.history.goBack.mockReset(); - props.history.push.mockReset(); - props.history.replace.mockReset(); - }); - - describe('mapStateToProps', () => { - const state = { - auth: { - token: '123-456', - enterpriseAccounts: [{} as EnterpriseAccount], - } as AuthState, - settings: { - participating: false, - } as SettingsState, - } as AppState; - it('should test the method', () => { - const mappedProps = mapStateToProps(state); - - expect(mappedProps.hasMultipleAccounts).toBeTruthy(); - expect(mappedProps.settings.participating).toBeFalsy(); - }); - - it('should recognize when only one account logged in', () => { - const mappedProps = mapStateToProps({ - ...state, - auth: { - ...state.auth, - token: null, - }, - }); - - expect(mappedProps.hasMultipleAccounts).toBeFalsy(); - }); - }); - - it('should render itself & its children', () => { - const tree = TestRenderer.create( - - - - ); - expect(tree).toMatchSnapshot(); - }); - - it('should press the logout', () => { - const { getByLabelText } = render(); - - fireEvent.click(getByLabelText('Logout')); - - expect(props.logout).toHaveBeenCalledTimes(1); - - expect(ipcRenderer.send).toHaveBeenCalledTimes(1); - expect(ipcRenderer.send).toHaveBeenCalledWith('update-icon'); - expect(props.history.goBack).toHaveBeenCalledTimes(1); - }); - - it('should call the componentWillReceiveProps method', function () { - const { rerender } = render(); - - expect(props.fetchNotifications).toHaveBeenCalledTimes(0); - - const updatedSettings = { - ...props.settings, - participating: !props.settings.participating, - }; - - rerender(); - - expect(props.fetchNotifications).toHaveBeenCalledTimes(1); - }); - - it('should go back by pressing the icon', () => { - const { getByLabelText } = render(); - fireEvent.click(getByLabelText('Go Back')); - expect(props.history.goBack).toHaveBeenCalledTimes(1); - }); - - it('should toggle the showOnlyParticipating checkbox', () => { - const { getByLabelText } = render(); - - fireEvent.click(getByLabelText('Show only participating'), { - target: { checked: true }, - }); - - expect(props.updateSetting).toHaveBeenCalledTimes(1); - }); - - it('should toggle the playSound checkbox', () => { - const { getByLabelText } = render(); - - fireEvent.click(getByLabelText('Play sound'), { - target: { checked: true }, - }); - - expect(props.updateSetting).toHaveBeenCalledTimes(1); - }); - - it('should toggle the showNotifications checkbox', () => { - const { getByLabelText } = render(); - - fireEvent.click(getByLabelText('Show notifications'), { - target: { checked: true }, - }); - - expect(props.updateSetting).toHaveBeenCalledTimes(1); - }); - - it('should toggle the onClickMarkAsRead checkbox', () => { - const { getByLabelText } = render(); - - fireEvent.click(getByLabelText('On Click, Mark as Read'), { - target: { checked: true }, - }); - - expect(props.updateSetting).toHaveBeenCalledTimes(1); - }); - - it('should toggle the openAtStartup checkbox', () => { - const { getByLabelText } = render(); - - fireEvent.click(getByLabelText('Open at startup'), { - target: { checked: true }, - }); - - expect(props.updateSetting).toHaveBeenCalledTimes(1); - }); - - it('should go to the enterprise login route', () => { - const { getByLabelText } = render( - - - - ); - fireEvent.click(getByLabelText('Login with GitHub Enterprise')); - expect(props.history.replace).toHaveBeenCalledWith('/enterpriselogin'); - }); - - it('should quit the app', () => { - const { getByLabelText } = render( - - - - ); - fireEvent.click(getByLabelText('Quit Gitify')); - expect(ipcRenderer.send).toHaveBeenCalledWith('app-quit'); - }); -}); diff --git a/src/js/routes/settings.tsx b/src/js/routes/settings.tsx deleted file mode 100644 index 77b4ab2d5..000000000 --- a/src/js/routes/settings.tsx +++ /dev/null @@ -1,193 +0,0 @@ -const { ipcRenderer, remote } = require('electron'); - -import * as React from 'react'; -import { connect } from 'react-redux'; -import { ArrowLeftIcon } from '@primer/octicons-react'; - -import { Appearance } from '../../types'; -import { AppState, SettingsState } from '../../types/reducers'; -import { fetchNotifications, updateSetting, logout } from '../actions'; -import { FieldCheckbox } from '../components/ui/checkbox'; -import { FieldRadioGroup } from '../components/fields/radiogroup'; -import { IconAddAccount } from '../../icons/AddAccount'; -import { IconLogOut } from '../../icons/Logout'; -import { IconQuit } from '../../icons/Quit'; -import { updateTrayIcon } from '../utils/comms'; - -const isLinux = remote.process.platform === 'linux'; - -interface IProps { - hasMultipleAccounts: boolean; - fetchNotifications: () => any; - logout: () => any; - settings: SettingsState; - updateSetting: any; - history: any; -} - -export class SettingsRoute extends React.Component { - constructor(props) { - super(props); - - this.state = { - participating: props.settings.participating, - }; - } - - static getDerivedStateFromProps(props, state) { - if (props.settings.participating !== state.participating) { - props.fetchNotifications(); - } - - return { - participating: props.settings.participating, - }; - } - - logout() { - this.props.logout(); - this.props.history.goBack(); - updateTrayIcon(); - } - - quitApp() { - ipcRenderer.send('app-quit'); - } - - goToEnterprise() { - return this.props.history.replace('/enterpriselogin'); - } - - render() { - const { settings } = this.props; - - const footerButtonClass = - 'hover:text-gray-500 py-1 px-2 my-1 mx-2 focus:outline-none'; - - return ( -
-
- - -

Settings

-
- -
- { - this.props.updateSetting('appearance', evt.target.value); - }} - /> - - - this.props.updateSetting('participating', evt.target.checked) - } - /> - - this.props.updateSetting('playSound', evt.target.checked) - } - /> - - this.props.updateSetting('showNotifications', evt.target.checked) - } - /> - - this.props.updateSetting('markOnClick', evt.target.checked) - } - /> - {!isLinux && ( - - this.props.updateSetting('openAtStartup', evt.target.checked) - } - /> - )} -
- -
- - Gitify v{remote.app.getVersion()} - - -
- - - - - -
-
-
- ); - } -} - -export function mapStateToProps(state: AppState) { - const enterpriseAccounts = state.auth.enterpriseAccounts; - const isGitHubLoggedIn = state.auth.token !== null; - const hasMultipleAccounts = isGitHubLoggedIn - ? enterpriseAccounts.length > 0 - : enterpriseAccounts.length > 1; - - return { - settings: state.settings, - hasMultipleAccounts, - }; -} - -export default connect(mapStateToProps, { - updateSetting, - fetchNotifications, - logout, -})(SettingsRoute); diff --git a/src/js/store/configureStore.test.ts b/src/js/store/configureStore.test.ts deleted file mode 100644 index 402a35aa0..000000000 --- a/src/js/store/configureStore.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import configureStore from './configureStore'; - -describe('store/configureStore.js', function () { - it('should load the store', function () { - const store = configureStore(); - - expect(store.dispatch).toBeDefined(); - expect(store.subscribe).toBeDefined(); - expect(store.getState).toBeDefined(); - expect(store.replaceReducer).toBeDefined(); - }); -}); diff --git a/src/js/store/configureStore.ts b/src/js/store/configureStore.ts deleted file mode 100644 index 53c738648..000000000 --- a/src/js/store/configureStore.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createStore, applyMiddleware } from 'redux'; -import thunkMiddleware from 'redux-thunk'; - -import * as storage from 'redux-storage'; -import createEngine from 'redux-storage-engine-localstorage'; -import filter from 'redux-storage-decorator-filter'; - -import { UPDATE_SETTING, LOGIN } from '../actions'; -import { LOGOUT } from '../../types/actions'; -import constants from '../utils/constants'; -import notificationsMiddlware from '../middleware/notifications'; -import rootReducer from '../reducers'; -import settingsMiddleware from '../middleware/settings'; -import { setAppearance } from '../utils/appearance'; - -export default function configureStore() { - const engine = filter(createEngine(constants.STORAGE_KEY), [ - 'settings', - ['auth', 'token'], - ['auth', 'enterpriseAccounts'], - ]); - - const storageMiddleware = storage.createMiddleware( - engine, - [], - [UPDATE_SETTING, LOGIN.SUCCESS, LOGOUT] - ); - - const middlewares = [ - thunkMiddleware, - notificationsMiddlware, - settingsMiddleware, - storageMiddleware, - ]; - - let store = createStore( - rootReducer, - undefined, - applyMiddleware(...middlewares) - ); - - // Load settings from localStorage - const load = storage.createLoader(engine); - load(store).then((state) => { - const { appearance } = state.settings; - setAppearance(appearance); - }); - - return store; -} diff --git a/src/js/utils/helpers.test.ts b/src/js/utils/helpers.test.ts deleted file mode 100644 index 41f19ba28..000000000 --- a/src/js/utils/helpers.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -const { remote } = require('electron'); -const BrowserWindow = remote.BrowserWindow; -const dialog = remote.dialog; - -import { - authGithub, - generateGitHubWebUrl, - generateGitHubAPIUrl, - isUserEitherLoggedIn, -} from './helpers'; -import { AuthState, EnterpriseAccount } from '../../types/reducers'; - -describe('utils/helpers.ts', () => { - it('should generate the GitHub url - non enterprise - (issue)', () => { - const apiUrl = - 'https://api.github.com/repos/ekonstantinidis/notifications-test/issues/3'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe( - 'https://github.com/ekonstantinidis/notifications-test/issues/3' - ); - }); - - it('should generate the GitHub url - non enterprise - (pull request)', () => { - const apiUrl = - 'https://api.github.com/repos/ekonstantinidis/notifications-test/pulls/123'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe( - 'https://github.com/ekonstantinidis/notifications-test/pull/123' - ); - }); - - it('should generate the GitHub url - non enterprise - (release)', () => { - const apiUrl = - 'https://api.github.com/repos/myorg/notifications-test/releases/3988077'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe('https://github.com/myorg/notifications-test/releases'); - }); - - it('should generate the GitHub url - enterprise - (issue)', () => { - const apiUrl = - 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/issues/123'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe( - 'https://github.gitify.io/myorg/notifications-test/issues/123' - ); - }); - - it('should generate the GitHub url - enterprise - (pull request)', () => { - const apiUrl = - 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/pulls/3'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe( - 'https://github.gitify.io/myorg/notifications-test/pull/3' - ); - }); - - it('should generate the GitHub url - enterprise - (release)', () => { - const apiUrl = - 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/releases/1'; - const newUrl = generateGitHubWebUrl(apiUrl); - expect(newUrl).toBe( - 'https://github.gitify.io/myorg/notifications-test/releases' - ); - }); - - it('should generate a GitHub API url - non enterprise', () => { - const result = generateGitHubAPIUrl('github.com'); - expect(result).toBe('https://api.github.com/'); - }); - - it('should generate a GitHub API url - enterprise', () => { - const result = generateGitHubAPIUrl('github.manos.im'); - expect(result).toBe('https://github.manos.im/api/v3/'); - }); - - it('should test isUserEitherLoggedIn - with github', () => { - const auth = { - token: '123-456', - enterpriseAccounts: [], - } as AuthState; - const result = isUserEitherLoggedIn(auth); - expect(result).toBeTruthy(); - }); - - it('should test isUserEitherLoggedIn - with enterprise', () => { - const auth = { - token: null, - enterpriseAccounts: [{} as EnterpriseAccount], - } as AuthState; - const result = isUserEitherLoggedIn(auth); - expect(result).toBeTruthy(); - }); - - it('should test the authGitHub - success', () => { - const dispatch = jest.fn(); - - spyOn(new BrowserWindow().webContents, 'on').and.callFake( - (event, callback) => { - if (event === 'will-redirect') { - const event = new Event('will-redirect'); - callback(event, 'http://github.com/?code=123-456'); - } - } - ); - - authGithub(undefined, dispatch); - - expect( - new BrowserWindow().webContents.session.clearStorageData - ).toHaveBeenCalledTimes(1); - - expect(new BrowserWindow().loadURL).toHaveBeenCalledTimes(1); - expect(new BrowserWindow().loadURL).toHaveBeenCalledWith( - 'https://github.com/login/oauth/authorize?client_id=FAKE_CLIENT_ID_123&scope=user:email,notifications' - ); - - expect(new BrowserWindow().destroy).toHaveBeenCalledTimes(1); - - expect(dispatch).toHaveBeenCalledTimes(1); - }); - - it('should test the authGitHub - with error', () => { - const dispatch = jest.fn(); - - spyOn(new BrowserWindow().webContents, 'on').and.callFake( - (event, callback) => { - if (event === 'will-redirect') { - const event = new Event('will-redirect'); - callback(event, 'http://www.github.com/?error=Oops'); - } - } - ); - - expect( - new BrowserWindow().webContents.session.clearStorageData - ).toHaveBeenCalledTimes(1); - - // @ts-ignore - new BrowserWindow().loadURL.mockReset(); - - authGithub(undefined, dispatch); - - expect(new BrowserWindow().loadURL).toHaveBeenCalledTimes(1); - - expect(window.alert).toHaveBeenCalledTimes(1); - expect(window.alert).toHaveBeenCalledWith( - "Oops! Something went wrong and we couldn't log you in using Github. Please try again." - ); - - expect(dispatch).not.toHaveBeenCalled(); - }); - - it('should test the authGitHub - fail to load the page', () => { - const dispatch = jest.fn(); - - // @ts-ignore - new BrowserWindow().loadURL.mockReset(); - // @ts-ignore - new BrowserWindow().destroy.mockReset(); - - spyOn(new BrowserWindow().webContents, 'on').and.callFake( - (event, callback) => { - if (event === 'did-fail-load') { - const event = new Event('did-fail-load'); - callback(event, 500, null, 'http://github.com/?code=123-456'); - } - } - ); - - authGithub(undefined, dispatch); - - expect(new BrowserWindow().loadURL).toHaveBeenCalledTimes(1); - expect(new BrowserWindow().loadURL).toHaveBeenCalledWith( - 'https://github.com/login/oauth/authorize?client_id=FAKE_CLIENT_ID_123&scope=user:email,notifications' - ); - - expect(new BrowserWindow().destroy).toHaveBeenCalledTimes(1); - - expect(dialog.showErrorBox).toHaveBeenCalledTimes(1); - expect(dialog.showErrorBox).toHaveBeenCalledWith( - 'Invalid Hostname', - 'Could not load https://github.com/.' - ); - - expect(dispatch).not.toHaveBeenCalled(); - }); - - it('should destroy the auth window on close', () => { - const dispatch = jest.fn(); - - spyOn(new BrowserWindow(), 'on').and.callFake((event, callback) => { - if (event === 'close') { - callback(); - } - }); - - // @ts-ignore - new BrowserWindow().destroy.mockReset(); - - authGithub(undefined, dispatch); - - expect(new BrowserWindow().destroy).toHaveBeenCalledTimes(1); - - expect(dispatch).not.toHaveBeenCalled(); - }); -}); diff --git a/src/js/utils/helpers.ts b/src/js/utils/helpers.ts deleted file mode 100644 index fac12063e..000000000 --- a/src/js/utils/helpers.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { parse } from 'url'; -const { remote } = require('electron'); -const BrowserWindow = remote.BrowserWindow; -const dialog = remote.dialog; - -import Constants from './constants'; -import { loginUser } from '../actions'; -import { AuthState } from '../../types/reducers'; - -export function getEnterpriseAccountToken(hostname, accounts): string { - return accounts.find((obj) => obj.hostname === hostname).token; -} - -export function generateGitHubAPIUrl(hostname) { - const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; - return isEnterprise - ? `https://${hostname}/api/v3/` - : `https://api.${hostname}/`; -} - -export function generateGitHubWebUrl(url: string) { - const { hostname } = parse(url); - const isEnterprise = - hostname !== `api.${Constants.DEFAULT_AUTH_OPTIONS.hostname}`; - - let newUrl: string = isEnterprise - ? url.replace(`${hostname}/api/v3/repos`, hostname) - : url.replace('api.github.com/repos', 'github.com'); - - if (newUrl.indexOf('/pulls/') !== -1) { - newUrl = newUrl.replace('/pulls/', '/pull/'); - } - - if (newUrl.indexOf('/releases/') !== -1) { - newUrl = newUrl.replace('/repos', ''); - newUrl = newUrl.substr(0, newUrl.lastIndexOf('/')); - } - - return newUrl; -} - -export function authGithub( - authOptions = Constants.DEFAULT_AUTH_OPTIONS, - dispatch -) { - // Build the OAuth consent page URL - const authWindow = new BrowserWindow({ - width: 800, - height: 600, - show: true, - }); - - const githubUrl = `https://${authOptions.hostname}/login/oauth/authorize`; - const authUrl = `${githubUrl}?client_id=${authOptions.clientId}&scope=${Constants.AUTH_SCOPE}`; - - const session = authWindow.webContents.session; - session.clearStorageData(); - - authWindow.loadURL(authUrl); - - function handleCallback(url) { - const raw_code = /code=([^&]*)/.exec(url) || null; - const code = raw_code && raw_code.length > 1 ? raw_code[1] : null; - const error = /\?error=(.+)$/.exec(url); - - if (code || error) { - // Close the browser if code found or error - authWindow.destroy(); - } - - // If there is a code, proceed to get token from github - if (code) { - dispatch(loginUser(authOptions, code)); - } else if (error) { - alert( - "Oops! Something went wrong and we couldn't " + - 'log you in using Github. Please try again.' - ); - } - } - - // If "Done" button is pressed, hide "Loading" - authWindow.on('close', () => { - authWindow.destroy(); - }); - - authWindow.webContents.on( - 'did-fail-load', - (event, errorCode, errorDescription, validatedURL) => { - if (validatedURL.includes(authOptions.hostname)) { - authWindow.destroy(); - dialog.showErrorBox( - 'Invalid Hostname', - `Could not load https://${authOptions.hostname}/.` - ); - } - } - ); - - authWindow.webContents.on('will-redirect', (event, url) => { - event.preventDefault(); - handleCallback(url); - }); -} - -export function isUserEitherLoggedIn(auth: AuthState) { - return auth.token !== null || auth.enterpriseAccounts.length > 0; -} diff --git a/src/js/utils/notifications.test.ts b/src/js/utils/notifications.test.ts deleted file mode 100644 index beeaa4bfa..000000000 --- a/src/js/utils/notifications.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import * as _ from 'lodash'; - -import { generateGitHubWebUrl } from '../utils/helpers'; -import { mockedGithubNotifications } from '../__mocks__/mockedData'; -import { SettingsState } from '../../types/reducers'; -import * as comms from './comms'; -import NotificationsUtils from '../utils/notifications'; - -describe('utils/notifications.ts', () => { - it('should raise a notification (settings - on)', () => { - const settings = { - playSound: true, - showNotifications: true, - } as SettingsState; - - spyOn(NotificationsUtils, 'raiseNativeNotification'); - spyOn(NotificationsUtils, 'raiseSoundNotification'); - - NotificationsUtils.setup( - mockedGithubNotifications, - mockedGithubNotifications.length, - settings - ); - - expect(NotificationsUtils.raiseNativeNotification).toHaveBeenCalledTimes(1); - expect(NotificationsUtils.raiseSoundNotification).toHaveBeenCalledTimes(1); - }); - - it('should not raise a notification (settings - off)', () => { - const settings = { - playSound: false, - showNotifications: false, - } as SettingsState; - - spyOn(NotificationsUtils, 'raiseNativeNotification'); - spyOn(NotificationsUtils, 'raiseSoundNotification'); - - NotificationsUtils.setup( - mockedGithubNotifications, - mockedGithubNotifications.length, - settings - ); - - expect(NotificationsUtils.raiseNativeNotification).not.toHaveBeenCalled(); - expect(NotificationsUtils.raiseSoundNotification).not.toHaveBeenCalled(); - }); - - it('should not raise a notification (because of 0(zero) notifications)', () => { - const settings = { - playSound: true, - showNotifications: true, - } as SettingsState; - - spyOn(NotificationsUtils, 'raiseNativeNotification'); - spyOn(NotificationsUtils, 'raiseSoundNotification'); - - NotificationsUtils.setup([], 0, settings); - - expect(NotificationsUtils.raiseNativeNotification).not.toHaveBeenCalled(); - expect(NotificationsUtils.raiseSoundNotification).not.toHaveBeenCalled(); - }); - - it('should raise a single native notification (with different icons)', () => { - const settings = { - playSound: false, - showNotifications: true, - } as SettingsState; - - const mockedNotification = mockedGithubNotifications[0]; - - spyOn(NotificationsUtils, 'raiseNativeNotification'); - spyOn(NotificationsUtils, 'raiseSoundNotification'); - - NotificationsUtils.setup([mockedNotification], 1, settings); - - expect(NotificationsUtils.raiseNativeNotification).toHaveBeenCalledTimes(1); - expect(NotificationsUtils.raiseSoundNotification).not.toHaveBeenCalled(); - - // @ts-ignore - NotificationsUtils.raiseNativeNotification.calls.reset(); - - // PullRequest - NotificationsUtils.setup( - _.updateWith( - [mockedNotification], - `[0][subject][type]`, - () => 'PullRequest' - ), - 1, - settings - ); - expect(NotificationsUtils.raiseNativeNotification).toHaveBeenCalledTimes(1); - }); - - it('should click on a native notification (with 1 notification)', () => { - spyOn(comms, 'openExternalLink'); - - const mockedNotifications = [[mockedGithubNotifications[0]]]; - - const nativeNotification: Notification = NotificationsUtils.raiseNativeNotification( - mockedNotifications, - 1 - ); - nativeNotification.onclick(null); - - const newUrl = generateGitHubWebUrl( - mockedGithubNotifications[0].subject.url - ); - expect(comms.openExternalLink).toHaveBeenCalledTimes(1); - expect(comms.openExternalLink).toHaveBeenCalledWith(newUrl); - }); - - it('should click on a native notification (with more than 1 notification)', () => { - spyOn(comms, 'reOpenWindow'); - - const mockedNotifications = [mockedGithubNotifications]; - const count = mockedGithubNotifications.length; - - const nativeNotification = NotificationsUtils.raiseNativeNotification( - mockedNotifications, - count - ); - nativeNotification.onclick(null); - - expect(comms.reOpenWindow).toHaveBeenCalledTimes(1); - }); - - it('should play a sound', () => { - spyOn(window.Audio.prototype, 'play'); - NotificationsUtils.raiseSoundNotification(); - expect(window.Audio.prototype.play).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/js/utils/notifications.ts b/src/js/utils/notifications.ts deleted file mode 100644 index b043a8d0f..000000000 --- a/src/js/utils/notifications.ts +++ /dev/null @@ -1,73 +0,0 @@ -const { remote } = require('electron'); - -import { generateGitHubWebUrl } from '../utils/helpers'; -import { reOpenWindow, openExternalLink } from '../utils/comms'; -import { Notification } from '../../types/github'; -import { SettingsState } from '../../types/reducers'; - -export default { - setup( - notifications: Notification[], - notificationsCount, - settings: SettingsState - ) { - // If there are no new notifications just stop there - if (!notificationsCount) { - return; - } - - if (settings.playSound) { - this.raiseSoundNotification(); - } - - if (settings.showNotifications) { - this.raiseNativeNotification(notifications, notificationsCount); - } - }, - - raiseNativeNotification(notifications, count: number) { - let title: string; - let body: string; - let notificationUrl: string | null; - - if (count === 1) { - const notification: Notification = notifications.find( - (obj) => obj.length > 0 - )[0]; - title = `Gitify - ${notification.repository.full_name}`; - body = notification.subject.title; - notificationUrl = notification.subject.url; - } else { - title = 'Gitify'; - body = `You have ${count} notifications.`; - } - - const nativeNotification = new Notification(title, { - body, - silent: true, - }); - - nativeNotification.onclick = function () { - if (count === 1) { - const appWindow = remote.getCurrentWindow(); - appWindow.hide(); - - // Some Notification types from GitHub are missing urls in their subjects. - if (notificationUrl) { - const url = generateGitHubWebUrl(notificationUrl); - openExternalLink(url); - } - } else { - reOpenWindow(); - } - }; - - return nativeNotification; - }, - - raiseSoundNotification() { - const audio = new Audio('assets/sounds/clearly.mp3'); - audio.volume = 0.2; - audio.play(); - }, -}; diff --git a/src/routes/Login.test.tsx b/src/routes/Login.test.tsx new file mode 100644 index 000000000..82a08f90a --- /dev/null +++ b/src/routes/Login.test.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import TestRenderer from 'react-test-renderer'; +import { Router } from 'react-router'; +import { MemoryRouter } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { render, fireEvent } from '@testing-library/react'; + +const { ipcRenderer } = require('electron'); + +import { AppContext } from '../context/App'; +import { LoginRoute } from './Login'; + +describe('routes/Login.tsx', () => { + const history = createMemoryHistory(); + const pushMock = jest.spyOn(history, 'push'); + const replaceMock = jest.spyOn(history, 'replace'); + + beforeEach(function () { + pushMock.mockReset(); + + spyOn(ipcRenderer, 'send'); + }); + + it('should render itself & its children', () => { + const tree = TestRenderer.create( + + + + ); + + expect(tree).toMatchSnapshot(); + }); + + it('should redirect to notifications once logged in', () => { + const { rerender } = render( + + + + + + ); + + rerender( + + + + + + ); + + expect(ipcRenderer.send).toHaveBeenCalledTimes(1); + expect(ipcRenderer.send).toHaveBeenCalledWith('reopen-window'); + expect(replaceMock).toHaveBeenCalledTimes(1); + }); + + it('should navigate to login with github enterprise', () => { + const { getByLabelText } = render( + + + + ); + + fireEvent.click(getByLabelText('Login with GitHub Enterprise')); + + expect(pushMock).toHaveBeenCalledTimes(1); + expect(pushMock).toHaveBeenCalledWith('/enterpriselogin'); + }); +}); diff --git a/src/routes/Login.tsx b/src/routes/Login.tsx new file mode 100644 index 000000000..61aa39367 --- /dev/null +++ b/src/routes/Login.tsx @@ -0,0 +1,56 @@ +const { ipcRenderer } = require('electron'); + +import React, { useCallback, useContext, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { AppContext } from '../context/App'; +import { Logo } from '../components/Logo'; + +export const LoginRoute: React.FC = () => { + const history = useHistory(); + const { isLoggedIn, login } = useContext(AppContext); + + useEffect(() => { + if (isLoggedIn) { + ipcRenderer.send('reopen-window'); + history.replace('/'); + } + }, [isLoggedIn]); + + const loginUser = useCallback(async () => { + try { + await login(); + } catch (err) { + // Skip + } + }, []); + + const loginButtonClass = + 'w-48 py-2 my-2 bg-gray-300 font-semibold rounded text-xs text-center dark:text-black hover:bg-gray-500 hover:text-white focus:outline-none'; + + return ( +
+ + +
+ GitHub Notifications
on your menu bar. +
+ + + + +
+ ); +}; diff --git a/src/routes/LoginEnterprise.test.tsx b/src/routes/LoginEnterprise.test.tsx new file mode 100644 index 000000000..f093ca9ae --- /dev/null +++ b/src/routes/LoginEnterprise.test.tsx @@ -0,0 +1,135 @@ +import * as React from 'react'; +import * as TestRenderer from 'react-test-renderer'; +import { fireEvent, render } from '@testing-library/react'; +import { Router } from 'react-router'; +import { MemoryRouter } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; + +const { ipcRenderer } = require('electron'); + +import { AppContext } from '../context/App'; +import { AuthState } from '../types'; +import { LoginEnterpriseRoute, validate } from './LoginEnterprise'; +import { mockedEnterpriseAccounts } from '../__mocks__/mockedData'; + +describe('routes/LoginEnterprise.js', () => { + const history = createMemoryHistory(); + const goBackMock = jest.spyOn(history, 'goBack'); + + const mockAccounts: AuthState = { + enterpriseAccounts: [], + }; + + beforeEach(function () { + goBackMock.mockReset(); + + spyOn(ipcRenderer, 'send'); + }); + + it('renders correctly', () => { + const tree = TestRenderer.create( + + + + + + ); + + expect(tree).toMatchSnapshot(); + }); + + it('let us go back', () => { + const goBackMock = jest.spyOn(history, 'goBack'); + + const { getByLabelText } = render( + + + + + + ); + + fireEvent.click(getByLabelText('Go Back')); + expect(goBackMock).toHaveBeenCalledTimes(1); + }); + + it('should validate the form values', () => { + let values; + const emptyValues = { + hostname: null, + clientId: null, + clientSecret: null, + }; + + values = { + ...emptyValues, + }; + expect(validate(values).hostname).toBe('Required'); + expect(validate(values).clientId).toBe('Required'); + expect(validate(values).clientSecret).toBe('Required'); + + values = { + ...emptyValues, + hostname: 'hello', + clientId: '!@£INVALID-.1', + clientSecret: '!@£INVALID-.1', + }; + expect(validate(values).hostname).toBe('Invalid hostname.'); + expect(validate(values).clientId).toBe('Invalid client id.'); + expect(validate(values).clientSecret).toBe('Invalid client secret.'); + }); + + it('should receive a logged-in enterprise account', () => { + const { rerender } = render( + + + + + + ); + + rerender( + + + + + + ); + + expect(ipcRenderer.send).toHaveBeenCalledTimes(1); + expect(ipcRenderer.send).toHaveBeenCalledWith('reopen-window'); + expect(goBackMock).toHaveBeenCalledTimes(1); + }); + + it('should render the form with errors', () => { + const { getByLabelText, getByTitle, getByText } = render( + + + + + + ); + + fireEvent.change(getByLabelText('Hostname'), { + target: { value: 'test' }, + }); + fireEvent.change(getByLabelText('Client ID'), { + target: { value: '123' }, + }); + fireEvent.change(getByLabelText('Client Secret'), { + target: { value: 'abc' }, + }); + + fireEvent.submit(getByTitle('Login Button')); + + expect(getByText('Invalid hostname.')).toBeTruthy(); + expect(getByText('Invalid client id.')).toBeTruthy(); + expect(getByText('Invalid client secret.')).toBeTruthy(); + }); +}); diff --git a/src/routes/LoginEnterprise.tsx b/src/routes/LoginEnterprise.tsx new file mode 100644 index 000000000..6282091f0 --- /dev/null +++ b/src/routes/LoginEnterprise.tsx @@ -0,0 +1,135 @@ +const ipcRenderer = require('electron').ipcRenderer; + +import React, { useCallback, useContext, useEffect } from 'react'; +import { Form, FormRenderProps } from 'react-final-form'; +import { ArrowLeftIcon } from '@primer/octicons-react'; +import { useHistory } from 'react-router-dom'; + +import { AppContext } from '../context/App'; +import { FieldInput } from '../components/fields/FieldInput'; +import { AuthOptions } from '../types'; + +interface IValues { + hostname?: string; + clientId?: string; + clientSecret?: string; +} + +interface IFormErrors { + hostname?: string; + clientId?: string; + clientSecret?: string; +} + +export const validate = (values: IValues): IFormErrors => { + const errors: IFormErrors = {}; + if (!values.hostname) { + errors.hostname = 'Required'; + } else if ( + !/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$/i.test( + values.hostname + ) + ) { + errors.hostname = 'Invalid hostname.'; + } + + if (!values.clientId) { + // 20 + errors.clientId = 'Required'; + } else if (!/^[A-Z0-9]{20}$/i.test(values.clientId)) { + errors.clientId = 'Invalid client id.'; + } + + if (!values.clientSecret) { + // 40 + errors.clientSecret = 'Required'; + } else if (!/^[A-Z0-9]{40}$/i.test(values.clientSecret)) { + errors.clientSecret = 'Invalid client secret.'; + } + + return errors; +}; + +export const LoginEnterpriseRoute: React.FC = () => { + const { + accounts: { enterpriseAccounts }, + loginEnterprise, + } = useContext(AppContext); + const history = useHistory(); + + useEffect(() => { + if (enterpriseAccounts.length) { + ipcRenderer.send('reopen-window'); + history.goBack(); + } + }, [enterpriseAccounts]); + + const renderForm = (formProps: FormRenderProps) => { + const { handleSubmit, submitting, pristine } = formProps; + + return ( +
+ + + + + + + + + ); + }; + + const login = useCallback(async (data: IValues) => { + try { + await loginEnterprise(data as AuthOptions); + } catch (err) { + // Skip + } + }, []); + + return ( +
+
+ + +

Login with GitHub Enterprise

+
+ +
+
+ {renderForm} +
+
+
+ ); +}; diff --git a/src/routes/Notifications.test.tsx b/src/routes/Notifications.test.tsx new file mode 100644 index 000000000..afeab1edd --- /dev/null +++ b/src/routes/Notifications.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import TestRenderer from 'react-test-renderer'; + +import { AppContext } from '../context/App'; +import { mockedAccountNotifications } from '../__mocks__/mockedData'; +import { NotificationsRoute } from './Notifications'; + +jest.mock('../components/AccountNotifications', () => ({ + AccountNotifications: 'AccountNotifications', +})); + +jest.mock('../components/AllRead', () => ({ + AllRead: 'AllRead', +})); + +jest.mock('../components/Oops', () => ({ + Oops: 'Oops', +})); + +describe('routes/Notifications.ts', () => { + it('should render itself & its children (with notifications)', () => { + const tree = TestRenderer.create( + + + + ); + + expect(tree).toMatchSnapshot(); + }); + + it('should render itself & its children (all read notifications)', () => { + const tree = TestRenderer.create( + + + + ); + expect(tree).toMatchSnapshot(); + }); + + it('should render itself & its children (error page - oops)', () => { + const tree = TestRenderer.create( + + + + ); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/routes/Notifications.tsx b/src/routes/Notifications.tsx new file mode 100644 index 000000000..247aa5695 --- /dev/null +++ b/src/routes/Notifications.tsx @@ -0,0 +1,43 @@ +import React, { useContext, useMemo } from 'react'; + +import { AccountNotifications } from '../components/AccountNotifications'; +import { AllRead } from '../components/AllRead'; +import { AppContext } from '../context/App'; +import { Oops } from '../components/Oops'; + +export const NotificationsRoute: React.FC = (props) => { + const { notifications, requestFailed } = useContext(AppContext); + + const hasMultipleAccounts = useMemo(() => notifications.length > 1, [ + notifications, + ]); + const notificationsCount = useMemo( + () => + notifications.reduce((memo, acc) => memo + acc.notifications.length, 0), + [notifications] + ); + const hasNotifications = useMemo(() => notificationsCount > 0, [ + notificationsCount, + ]); + + if (requestFailed) { + return ; + } + + if (!hasNotifications) { + return ; + } + + return ( +
+ {notifications.map((account) => ( + + ))} +
+ ); +}; diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx new file mode 100644 index 000000000..54277e6b4 --- /dev/null +++ b/src/routes/Settings.test.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import TestRenderer from 'react-test-renderer'; +import { render, fireEvent } from '@testing-library/react'; +import { Router } from 'react-router'; +import { MemoryRouter } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; + +const { ipcRenderer } = require('electron'); + +import { SettingsRoute } from './Settings'; +import { AppContext } from '../context/App'; +import { mockSettings } from '../__mocks__/mock-state'; + +describe('routes/Settings.tsx', () => { + const history = createMemoryHistory(); + const goBackMock = jest.spyOn(history, 'goBack'); + const replaceMock = jest.spyOn(history, 'replace'); + const updateSetting = jest.fn(); + + beforeEach(() => { + goBackMock.mockReset(); + updateSetting.mockReset(); + }); + + it('should render itself & its children', () => { + const tree = TestRenderer.create( + + + + + + ); + expect(tree).toMatchSnapshot(); + }); + + it('should press the logout', () => { + const logoutMock = jest.fn(); + + const { getByLabelText } = render( + + + + + + ); + + fireEvent.click(getByLabelText('Logout')); + + expect(logoutMock).toHaveBeenCalledTimes(1); + + expect(ipcRenderer.send).toHaveBeenCalledTimes(1); + expect(ipcRenderer.send).toHaveBeenCalledWith('update-icon'); + expect(goBackMock).toHaveBeenCalledTimes(1); + }); + + it('should go back by pressing the icon', () => { + const { getByLabelText } = render( + + + + + + ); + fireEvent.click(getByLabelText('Go Back')); + expect(goBackMock).toHaveBeenCalledTimes(1); + }); + + it('should toggle the showOnlyParticipating checkbox', () => { + const { getByLabelText } = render( + + + + + + ); + + fireEvent.click(getByLabelText('Show only participating'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + }); + + it('should toggle the playSound checkbox', () => { + const { getByLabelText } = render( + + + + + + ); + + fireEvent.click(getByLabelText('Play sound'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + }); + + it('should toggle the showNotifications checkbox', () => { + const { getByLabelText } = render( + + + + + + ); + + fireEvent.click(getByLabelText('Show notifications'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + }); + + it('should toggle the onClickMarkAsRead checkbox', () => { + const { getByLabelText } = render( + + + + + + ); + + fireEvent.click(getByLabelText('On Click, Mark as Read'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + }); + + it('should toggle the openAtStartup checkbox', () => { + const { getByLabelText } = render( + + + + + + ); + + fireEvent.click(getByLabelText('Open at startup'), { + target: { checked: true }, + }); + + expect(updateSetting).toHaveBeenCalledTimes(1); + }); + + it('should go to the enterprise login route', () => { + const { getByLabelText } = render( + + + + + + ); + fireEvent.click(getByLabelText('Login with GitHub Enterprise')); + expect(replaceMock).toHaveBeenCalledWith('/enterpriselogin'); + }); + + it('should quit the app', () => { + const { getByLabelText } = render( + + + + + + ); + fireEvent.click(getByLabelText('Quit Gitify')); + expect(ipcRenderer.send).toHaveBeenCalledWith('app-quit'); + }); +}); diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx new file mode 100644 index 000000000..39975cc46 --- /dev/null +++ b/src/routes/Settings.tsx @@ -0,0 +1,139 @@ +const { ipcRenderer, remote } = require('electron'); + +import React, { useCallback, useContext } from 'react'; +import { useHistory } from 'react-router-dom'; +import { ArrowLeftIcon } from '@primer/octicons-react'; + +import { AppContext } from '../context/App'; +import { Appearance } from '../types'; +import { FieldCheckbox } from '../components/fields/Checkbox'; +import { FieldRadioGroup } from '../components/fields/RadioGroup'; +import { IconAddAccount } from '../icons/AddAccount'; +import { IconLogOut } from '../icons/Logout'; +import { IconQuit } from '../icons/Quit'; +import { updateTrayIcon } from '../utils/comms'; + +const isLinux = remote.process.platform === 'linux'; + +export const SettingsRoute: React.FC = () => { + const { settings, updateSetting, logout } = useContext(AppContext); + const history = useHistory(); + + const logoutUser = useCallback(() => { + logout(); + history.goBack(); + updateTrayIcon(); + }, []); + + const quitApp = useCallback(() => { + ipcRenderer.send('app-quit'); + }, []); + + const goToEnterprise = useCallback(() => { + return history.replace('/enterpriselogin'); + }, []); + + const footerButtonClass = + 'hover:text-gray-500 py-1 px-2 my-1 mx-2 focus:outline-none'; + + return ( +
+
+ + +

Settings

+
+ +
+ { + updateSetting('appearance', evt.target.value); + }} + /> + + updateSetting('participating', evt.target.checked)} + /> + updateSetting('playSound', evt.target.checked)} + /> + + updateSetting('showNotifications', evt.target.checked) + } + /> + updateSetting('markOnClick', evt.target.checked)} + /> + {!isLinux && ( + + updateSetting('openAtStartup', evt.target.checked) + } + /> + )} +
+ +
+ + Gitify v{remote.app.getVersion()} + + +
+ + + + + +
+
+
+ ); +}; diff --git a/src/js/routes/__snapshots__/login.test.tsx.snap b/src/routes/__snapshots__/Login.test.tsx.snap similarity index 97% rename from src/js/routes/__snapshots__/login.test.tsx.snap rename to src/routes/__snapshots__/Login.test.tsx.snap index b9d110a30..c590bc50c 100644 --- a/src/js/routes/__snapshots__/login.test.tsx.snap +++ b/src/routes/__snapshots__/Login.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`routes/login.tsx should render itself & its children 1`] = ` +exports[`routes/Login.tsx should render itself & its children 1`] = `
diff --git a/src/js/routes/__snapshots__/enterprise-login.test.tsx.snap b/src/routes/__snapshots__/LoginEnterprise.test.tsx.snap similarity index 98% rename from src/js/routes/__snapshots__/enterprise-login.test.tsx.snap rename to src/routes/__snapshots__/LoginEnterprise.test.tsx.snap index 67beca15d..cfe52f9e9 100644 --- a/src/js/routes/__snapshots__/enterprise-login.test.tsx.snap +++ b/src/routes/__snapshots__/LoginEnterprise.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`routes/enterprise-login.js renders correctly 1`] = ` +exports[`routes/LoginEnterprise.js renders correctly 1`] = `
diff --git a/src/js/routes/__snapshots__/notifications.test.tsx.snap b/src/routes/__snapshots__/Notifications.test.tsx.snap similarity index 98% rename from src/js/routes/__snapshots__/notifications.test.tsx.snap rename to src/routes/__snapshots__/Notifications.test.tsx.snap index 141f45519..f878e4979 100644 --- a/src/js/routes/__snapshots__/notifications.test.tsx.snap +++ b/src/routes/__snapshots__/Notifications.test.tsx.snap @@ -1,10 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`routes/notifications.ts should render itself & its children (all read notifications) 1`] = ``; +exports[`routes/Notifications.ts should render itself & its children (all read notifications) 1`] = ``; -exports[`routes/notifications.ts should render itself & its children (error page - oops) 1`] = ``; +exports[`routes/Notifications.ts should render itself & its children (error page - oops) 1`] = ``; -exports[`routes/notifications.ts should render itself & its children (with notifications) 1`] = ` +exports[`routes/Notifications.ts should render itself & its children (with notifications) 1`] = `
diff --git a/src/js/routes/__snapshots__/settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap similarity index 99% rename from src/js/routes/__snapshots__/settings.test.tsx.snap rename to src/routes/__snapshots__/Settings.test.tsx.snap index 63c95131f..1be030839 100644 --- a/src/js/routes/__snapshots__/settings.test.tsx.snap +++ b/src/routes/__snapshots__/Settings.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`routes/settings.tsx should render itself & its children 1`] = ` +exports[`routes/Settings.tsx should render itself & its children 1`] = `
@@ -63,7 +63,7 @@ exports[`routes/settings.tsx should render itself & its children 1`] = ` className="flex items-center mt-1" > document.querySelector('html').classList.remove('dark'); diff --git a/src/utils/auth.test.ts b/src/utils/auth.test.ts new file mode 100644 index 000000000..b0c0992a4 --- /dev/null +++ b/src/utils/auth.test.ts @@ -0,0 +1,100 @@ +import { AxiosPromise, AxiosResponse } from 'axios'; + +const { remote } = require('electron'); +const BrowserWindow = remote.BrowserWindow; + +import * as auth from './auth'; +import * as apiRequests from './api-requests'; + +describe('utils/auth.tsx', () => { + describe('authGitHub', () => { + const loadURLMock = jest.spyOn(new BrowserWindow(), 'loadURL'); + + beforeEach(() => { + loadURLMock.mockReset(); + }); + + it('should call authGithub - success', async () => { + spyOn(new BrowserWindow().webContents, 'on').and.callFake( + (event, callback) => { + if (event === 'will-redirect') { + const event = new Event('will-redirect'); + callback(event, 'http://github.com/?code=123-456'); + } + } + ); + + const res = await auth.authGitHub(); + + expect(res.authCode).toBe('123-456'); + + expect( + new BrowserWindow().webContents.session.clearStorageData + ).toHaveBeenCalledTimes(1); + + expect(loadURLMock).toHaveBeenCalledTimes(1); + expect(loadURLMock).toHaveBeenCalledWith( + 'https://github.com/login/oauth/authorize?client_id=FAKE_CLIENT_ID_123&scope=user:email,notifications' + ); + + expect(new BrowserWindow().destroy).toHaveBeenCalledTimes(1); + }); + + it('should call authGithub - failure', async () => { + spyOn(new BrowserWindow().webContents, 'on').and.callFake( + (event, callback) => { + if (event === 'will-redirect') { + const event = new Event('will-redirect'); + callback(event, 'http://www.github.com/?error=Oops'); + } + } + ); + + await expect(async () => await auth.authGitHub()).rejects.toEqual( + "Oops! Something went wrong and we couldn't log you in using Github. Please try again." + ); + expect(loadURLMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('getToken', () => { + const authCode = '123-456'; + const apiRequestMock = jest.spyOn(apiRequests, 'apiRequest'); + + it('should get a token - success', async () => { + const requestPromise = new Promise((resolve) => + resolve({ data: { access_token: 'this-is-a-token' } } as AxiosResponse) + ) as AxiosPromise; + + apiRequestMock.mockResolvedValueOnce(requestPromise); + + const res = await auth.getToken(authCode); + + expect(apiRequests.apiRequest).toHaveBeenCalledWith( + 'https://github.com/login/oauth/access_token', + 'POST', + { + client_id: 'FAKE_CLIENT_ID_123', + client_secret: 'FAKE_CLIENT_SECRET_123', + code: '123-456', + } + ); + expect(res.token).toBe('this-is-a-token'); + expect(res.hostname).toBe('github.com'); + }); + + it('should get a token - failure', async () => { + const message = 'Something went wrong.'; + + const requestPromise = new Promise((_, reject) => + reject({ data: { message } } as AxiosResponse) + ) as AxiosPromise; + + apiRequestMock.mockResolvedValueOnce(requestPromise); + + const call = async () => await auth.getToken(authCode); + + await expect(call).rejects.toEqual({ data: { message } }); + }); + }); +}); diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 000000000..6bb3d6da4 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,86 @@ +const { remote } = require('electron'); +const BrowserWindow = remote.BrowserWindow; + +import { apiRequest } from '../utils/api-requests'; +import { AuthResponse, AuthTokenResponse } from '../types'; +import { Constants } from '../utils/constants'; + +export const authGitHub = ( + authOptions = Constants.DEFAULT_AUTH_OPTIONS +): Promise => { + return new Promise((resolve, reject) => { + // Build the OAuth consent page URL + const authWindow = new BrowserWindow({ + width: 800, + height: 600, + show: true, + }); + + const githubUrl = `https://${authOptions.hostname}/login/oauth/authorize`; + const authUrl = `${githubUrl}?client_id=${authOptions.clientId}&scope=${Constants.AUTH_SCOPE}`; + + const session = authWindow.webContents.session; + session.clearStorageData(); + + authWindow.loadURL(authUrl); + + const handleCallback = (url: string) => { + const raw_code = /code=([^&]*)/.exec(url) || null; + const authCode = raw_code && raw_code.length > 1 ? raw_code[1] : null; + const error = /\?error=(.+)$/.exec(url); + if (authCode || error) { + // Close the browser if code found or error + authWindow.destroy(); + } + // If there is a code, proceed to get token from github + if (authCode) { + resolve({ authCode, authOptions }); + } else if (error) { + reject( + "Oops! Something went wrong and we couldn't " + + 'log you in using Github. Please try again.' + ); + } + }; + + // If "Done" button is pressed, hide "Loading" + authWindow.on('close', () => { + authWindow.destroy(); + }); + + authWindow.webContents.on( + 'did-fail-load', + (event, errorCode, errorDescription, validatedURL) => { + if (validatedURL.includes(authOptions.hostname)) { + authWindow.destroy(); + reject( + `Invalid Hostname. Could not load https://${authOptions.hostname}/.` + ); + } + } + ); + + authWindow.webContents.on('will-redirect', (event, url) => { + event.preventDefault(); + handleCallback(url); + }); + }); +}; + +export const getToken = async ( + authCode: string, + authOptions = Constants.DEFAULT_AUTH_OPTIONS +): Promise => { + const url = `https://${authOptions.hostname}/login/oauth/access_token`; + const data = { + client_id: authOptions.clientId, + client_secret: authOptions.clientSecret, + code: authCode, + }; + + const response = await apiRequest(url, 'POST', data); + return { + hostname: authOptions.hostname, + token: response.data.access_token, + }; +}; diff --git a/src/js/utils/comms.test.ts b/src/utils/comms.test.ts similarity index 100% rename from src/js/utils/comms.test.ts rename to src/utils/comms.test.ts diff --git a/src/js/utils/comms.ts b/src/utils/comms.ts similarity index 59% rename from src/js/utils/comms.ts rename to src/utils/comms.ts index 82be8ceea..6b2b5a33c 100644 --- a/src/js/utils/comms.ts +++ b/src/utils/comms.ts @@ -1,19 +1,17 @@ const { ipcRenderer, remote, shell } = require('electron'); -import { SettingsState } from '../../types/reducers'; - -export function openExternalLink(url) { +export function openExternalLink(url: string): void { shell.openExternal(url); } -export function setAutoLaunch(value) { +export function setAutoLaunch(value: boolean): void { remote.app.setLoginItemSettings({ openAtLogin: value, openAsHidden: value, }); } -export function updateTrayIcon(notificationsLength = 0) { +export function updateTrayIcon(notificationsLength = 0): void { if (notificationsLength > 0) { ipcRenderer.send('update-icon', 'TrayActive'); } else { @@ -21,10 +19,10 @@ export function updateTrayIcon(notificationsLength = 0) { } } -export function reOpenWindow() { +export function reOpenWindow(): void { ipcRenderer.send('reopen-window'); } -export function restoreSetting(setting, value) { +export function restoreSetting(setting, value): void { ipcRenderer.send(setting, value); } diff --git a/src/js/utils/constants.tsx b/src/utils/constants.ts similarity index 93% rename from src/js/utils/constants.tsx rename to src/utils/constants.ts index ec8bec046..c97d83b8d 100644 --- a/src/js/utils/constants.tsx +++ b/src/utils/constants.ts @@ -1,4 +1,4 @@ -export default { +export const Constants = { // GitHub OAuth AUTH_SCOPE: ['user:email', 'notifications'], @@ -42,3 +42,5 @@ export default { ':cry:', ], }; + +export default Constants; diff --git a/src/js/utils/github-api.test.ts b/src/utils/github-api.test.ts similarity index 96% rename from src/js/utils/github-api.test.ts rename to src/utils/github-api.test.ts index e0b180f00..a8e50d073 100644 --- a/src/js/utils/github-api.test.ts +++ b/src/utils/github-api.test.ts @@ -1,5 +1,5 @@ import { formatReason, getNotificationTypeIcon } from './github-api'; -import { Reason, SubjectType } from '../../types/github'; +import { Reason, SubjectType } from '../typesGithub'; describe('./utils/github-api.ts', () => { it('should format the notification reason', () => { diff --git a/src/js/utils/github-api.ts b/src/utils/github-api.ts similarity index 98% rename from src/js/utils/github-api.ts rename to src/utils/github-api.ts index 521ebf465..73737520f 100644 --- a/src/js/utils/github-api.ts +++ b/src/utils/github-api.ts @@ -1,4 +1,4 @@ -import { Reason, SubjectType } from '../../types/github'; +import { Reason, SubjectType } from '../typesGithub'; import * as Octicons from '@primer/octicons-react'; // prettier-ignore diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts new file mode 100644 index 000000000..05951fed2 --- /dev/null +++ b/src/utils/helpers.test.ts @@ -0,0 +1,65 @@ +import { generateGitHubWebUrl, generateGitHubAPIUrl } from './helpers'; + +describe('utils/helpers.ts', () => { + it('should generate the GitHub url - non enterprise - (issue)', () => { + const apiUrl = + 'https://api.github.com/repos/ekonstantinidis/notifications-test/issues/3'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe( + 'https://github.com/ekonstantinidis/notifications-test/issues/3' + ); + }); + + it('should generate the GitHub url - non enterprise - (pull request)', () => { + const apiUrl = + 'https://api.github.com/repos/ekonstantinidis/notifications-test/pulls/123'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe( + 'https://github.com/ekonstantinidis/notifications-test/pull/123' + ); + }); + + it('should generate the GitHub url - non enterprise - (release)', () => { + const apiUrl = + 'https://api.github.com/repos/myorg/notifications-test/releases/3988077'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe('https://github.com/myorg/notifications-test/releases'); + }); + + it('should generate the GitHub url - enterprise - (issue)', () => { + const apiUrl = + 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/issues/123'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe( + 'https://github.gitify.io/myorg/notifications-test/issues/123' + ); + }); + + it('should generate the GitHub url - enterprise - (pull request)', () => { + const apiUrl = + 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/pulls/3'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe( + 'https://github.gitify.io/myorg/notifications-test/pull/3' + ); + }); + + it('should generate the GitHub url - enterprise - (release)', () => { + const apiUrl = + 'https://github.gitify.io/api/v3/repos/myorg/notifications-test/releases/1'; + const newUrl = generateGitHubWebUrl(apiUrl); + expect(newUrl).toBe( + 'https://github.gitify.io/myorg/notifications-test/releases' + ); + }); + + it('should generate a GitHub API url - non enterprise', () => { + const result = generateGitHubAPIUrl('github.com'); + expect(result).toBe('https://api.github.com/'); + }); + + it('should generate a GitHub API url - enterprise', () => { + const result = generateGitHubAPIUrl('github.manos.im'); + expect(result).toBe('https://github.manos.im/api/v3/'); + }); +}); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts new file mode 100644 index 000000000..515a598e9 --- /dev/null +++ b/src/utils/helpers.ts @@ -0,0 +1,39 @@ +import { parse } from 'url'; +import { EnterpriseAccount } from '../types'; + +import { Constants } from './constants'; + +export function getEnterpriseAccountToken( + hostname: string, + accounts: EnterpriseAccount[] +): string { + return accounts.find((obj) => obj.hostname === hostname).token; +} + +export function generateGitHubAPIUrl(hostname) { + const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname; + return isEnterprise + ? `https://${hostname}/api/v3/` + : `https://api.${hostname}/`; +} + +export function generateGitHubWebUrl(url: string) { + const { hostname } = parse(url); + const isEnterprise = + hostname !== `api.${Constants.DEFAULT_AUTH_OPTIONS.hostname}`; + + let newUrl: string = isEnterprise + ? url.replace(`${hostname}/api/v3/repos`, hostname) + : url.replace('api.github.com/repos', 'github.com'); + + if (newUrl.indexOf('/pulls/') !== -1) { + newUrl = newUrl.replace('/pulls/', '/pull/'); + } + + if (newUrl.indexOf('/releases/') !== -1) { + newUrl = newUrl.replace('/repos', ''); + newUrl = newUrl.substr(0, newUrl.lastIndexOf('/')); + } + + return newUrl; +} diff --git a/src/utils/notifications.test.ts b/src/utils/notifications.test.ts new file mode 100644 index 000000000..f11e4910c --- /dev/null +++ b/src/utils/notifications.test.ts @@ -0,0 +1,126 @@ +import * as _ from 'lodash'; + +import { generateGitHubWebUrl } from './helpers'; +import { + mockedAccountNotifications, + mockedGithubNotifications, + mockedSingleAccountNotifications, +} from '../__mocks__/mockedData'; +import * as comms from './comms'; +import * as notificationsHelpers from './notifications'; +import { SettingsState } from '../types'; +import { defaultSettings } from '../context/App'; + +describe('utils/notifications.ts', () => { + it('should raise a notification (settings - on)', () => { + const settings: SettingsState = { + ...defaultSettings, + playSound: true, + showNotifications: true, + }; + + jest.spyOn(notificationsHelpers, 'raiseNativeNotification'); + jest.spyOn(notificationsHelpers, 'raiseSoundNotification'); + + notificationsHelpers.triggerNativeNotifications( + [], + mockedAccountNotifications, + settings + ); + + expect(notificationsHelpers.raiseNativeNotification).toHaveBeenCalledTimes( + 1 + ); + expect(notificationsHelpers.raiseSoundNotification).toHaveBeenCalledTimes( + 1 + ); + }); + + it('should not raise a notification (settings - off)', () => { + const settings = { + ...defaultSettings, + playSound: false, + showNotifications: false, + }; + + spyOn(notificationsHelpers, 'raiseNativeNotification'); + spyOn(notificationsHelpers, 'raiseSoundNotification'); + + notificationsHelpers.triggerNativeNotifications( + [], + mockedAccountNotifications, + settings + ); + + expect(notificationsHelpers.raiseNativeNotification).not.toHaveBeenCalled(); + expect(notificationsHelpers.raiseSoundNotification).not.toHaveBeenCalled(); + }); + + it('should not raise a notification or play a sound (no new notifications)', () => { + const settings = { + ...defaultSettings, + playSound: true, + showNotifications: true, + }; + + spyOn(notificationsHelpers, 'raiseNativeNotification'); + spyOn(notificationsHelpers, 'raiseSoundNotification'); + + notificationsHelpers.triggerNativeNotifications( + mockedSingleAccountNotifications, + mockedSingleAccountNotifications, + settings + ); + + expect(notificationsHelpers.raiseNativeNotification).not.toHaveBeenCalled(); + expect(notificationsHelpers.raiseSoundNotification).not.toHaveBeenCalled(); + }); + + it('should not raise a notification (because of 0(zero) notifications)', () => { + const settings = { + ...defaultSettings, + playSound: true, + showNotifications: true, + }; + + spyOn(notificationsHelpers, 'raiseNativeNotification'); + spyOn(notificationsHelpers, 'raiseSoundNotification'); + + notificationsHelpers.triggerNativeNotifications([], [], settings); + + expect(notificationsHelpers.raiseNativeNotification).not.toHaveBeenCalled(); + expect(notificationsHelpers.raiseSoundNotification).not.toHaveBeenCalled(); + }); + + it('should click on a native notification (with 1 notification)', () => { + spyOn(comms, 'openExternalLink'); + + const nativeNotification: Notification = notificationsHelpers.raiseNativeNotification( + [mockedGithubNotifications[0]] + ); + nativeNotification.onclick(null); + + const newUrl = generateGitHubWebUrl( + mockedGithubNotifications[0].subject.url + ); + expect(comms.openExternalLink).toHaveBeenCalledTimes(1); + expect(comms.openExternalLink).toHaveBeenCalledWith(newUrl); + }); + + it('should click on a native notification (with more than 1 notification)', () => { + spyOn(comms, 'reOpenWindow'); + + const nativeNotification = notificationsHelpers.raiseNativeNotification( + mockedGithubNotifications + ); + nativeNotification.onclick(null); + + expect(comms.reOpenWindow).toHaveBeenCalledTimes(1); + }); + + it('should play a sound', () => { + spyOn(window.Audio.prototype, 'play'); + notificationsHelpers.raiseSoundNotification(); + expect(window.Audio.prototype.play).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts new file mode 100644 index 000000000..911c604e8 --- /dev/null +++ b/src/utils/notifications.ts @@ -0,0 +1,103 @@ +const { remote } = require('electron'); + +import { generateGitHubWebUrl } from './helpers'; +import { reOpenWindow, openExternalLink, updateTrayIcon } from './comms'; +import { Notification } from '../typesGithub'; + +import { AccountNotifications, SettingsState } from '../types'; + +export const setTrayIconColor = (notifications: AccountNotifications[]) => { + const allNotificationsCount = notifications.reduce( + (memo, acc) => memo + acc.notifications.length, + 0 + ); + + updateTrayIcon(allNotificationsCount); +}; + +export const triggerNativeNotifications = ( + previousNotifications: AccountNotifications[], + newNotifications: AccountNotifications[], + settings: SettingsState +) => { + const diffNotifications = newNotifications + .map((account) => { + const accountPreviousNotifications = previousNotifications.find( + (item) => item.hostname === account.hostname + ); + + if (!accountPreviousNotifications) { + return account.notifications; + } + + const accountPreviousNotificationsIds = accountPreviousNotifications.notifications.map( + (item) => item.id + ); + + const accountNewNotifications = account.notifications.filter((item) => { + return !accountPreviousNotificationsIds.includes(`${item.id}`); + }); + + return accountNewNotifications; + }) + .reduce((acc, val) => acc.concat(val), []); + + setTrayIconColor(newNotifications); + + // If there are no new notifications just stop there + if (!diffNotifications.length) { + return; + } + + if (settings.playSound) { + raiseSoundNotification(); + } + + if (settings.showNotifications) { + raiseNativeNotification(diffNotifications); + } +}; + +export const raiseNativeNotification = (notifications: Notification[]) => { + let title: string; + let body: string; + let notificationUrl: string | null; + + if (notifications.length === 1) { + const notification = notifications[0]; + title = `Gitify - ${notification.repository.full_name}`; + body = notification.subject.title; + notificationUrl = notification.subject.url; + } else { + title = 'Gitify'; + body = `You have ${notifications.length} notifications.`; + } + + const nativeNotification = new Notification(title, { + body, + silent: true, + }); + + nativeNotification.onclick = function () { + if (notifications.length === 1) { + const appWindow = remote.getCurrentWindow(); + appWindow.hide(); + + // Some Notification types from GitHub are missing urls in their subjects. + if (notificationUrl) { + const url = generateGitHubWebUrl(notificationUrl); + openExternalLink(url); + } + } else { + reOpenWindow(); + } + }; + + return nativeNotification; +}; + +export const raiseSoundNotification = () => { + const audio = new Audio('assets/sounds/clearly.mp3'); + audio.volume = 0.2; + audio.play(); +}; diff --git a/src/utils/remove-notification.test.ts b/src/utils/remove-notification.test.ts new file mode 100644 index 000000000..fd070e819 --- /dev/null +++ b/src/utils/remove-notification.test.ts @@ -0,0 +1,24 @@ +import * as _ from 'lodash'; + +import { + mockedSingleAccountNotifications, + mockedSingleNotification, +} from '../__mocks__/mockedData'; +import { removeNotification } from './remove-notification'; + +describe('utils/remove-notification.ts', () => { + const notificationId = mockedSingleNotification.id; + const hostname = mockedSingleAccountNotifications[0].hostname; + + it('should remove a notifiction if it exists', () => { + expect(mockedSingleAccountNotifications[0].notifications.length).toBe(1); + + const result = removeNotification( + notificationId, + mockedSingleAccountNotifications, + hostname + ); + + expect(result[0].notifications.length).toBe(0); + }); +}); diff --git a/src/utils/remove-notification.ts b/src/utils/remove-notification.ts new file mode 100644 index 000000000..d4dfaac30 --- /dev/null +++ b/src/utils/remove-notification.ts @@ -0,0 +1,22 @@ +import updateWith from 'lodash/updateWith'; + +import { AccountNotifications } from '../types'; +import { Notification } from '../typesGithub'; + +export const removeNotification = ( + id: string, + notifications: AccountNotifications[], + hostname: string +): AccountNotifications[] => { + const accountIndex = notifications.findIndex( + (accountNotifications) => accountNotifications.hostname === hostname + ); + + return updateWith( + [...notifications], + `[${accountIndex}][notifications]`, + (accNotifications: Notification[] = []) => { + return accNotifications.filter((notification) => notification.id !== id); + } + ); +}; diff --git a/src/utils/remove-notifications.test.ts b/src/utils/remove-notifications.test.ts new file mode 100644 index 000000000..a046d327b --- /dev/null +++ b/src/utils/remove-notifications.test.ts @@ -0,0 +1,37 @@ +import { + mockedAccountNotifications, + mockedSingleAccountNotifications, + mockedSingleNotification, +} from '../__mocks__/mockedData'; +import { removeNotifications } from './remove-notifications'; + +describe('utils/remove-notification.ts', () => { + const repoSlug = mockedSingleNotification.repository.full_name; + const hostname = mockedSingleAccountNotifications[0].hostname; + + it("should remove a repo's notifications - single", () => { + expect(mockedSingleAccountNotifications[0].notifications.length).toBe(1); + + const result = removeNotifications( + repoSlug, + mockedSingleAccountNotifications, + hostname + ); + + expect(result[0].notifications.length).toBe(0); + }); + + it("should remove a repo's notifications - multiple", () => { + expect(mockedAccountNotifications[0].notifications.length).toBe(2); + expect(mockedAccountNotifications[1].notifications.length).toBe(2); + + const result = removeNotifications( + repoSlug, + mockedAccountNotifications, + hostname + ); + + expect(result[0].notifications.length).toBe(0); + expect(result[1].notifications.length).toBe(2); + }); +}); diff --git a/src/utils/remove-notifications.ts b/src/utils/remove-notifications.ts new file mode 100644 index 000000000..59eb4872a --- /dev/null +++ b/src/utils/remove-notifications.ts @@ -0,0 +1,24 @@ +import updateWith from 'lodash/updateWith'; + +import { AccountNotifications } from '../types'; +import { Notification } from '../typesGithub'; + +export const removeNotifications = ( + repoSlug: string, + notifications: AccountNotifications[], + hostname: string +): AccountNotifications[] => { + const accountIndex = notifications.findIndex( + (accountNotifications) => accountNotifications.hostname === hostname + ); + + return updateWith( + [...notifications], + `[${accountIndex}][notifications]`, + (accNotifications: Notification[] = []) => { + return accNotifications.filter( + (notification) => notification.repository.full_name !== repoSlug + ); + } + ); +}; diff --git a/src/utils/storage.test.ts b/src/utils/storage.test.ts new file mode 100644 index 000000000..175a1fa5b --- /dev/null +++ b/src/utils/storage.test.ts @@ -0,0 +1,43 @@ +import { mockSettings } from '../__mocks__/mock-state'; +import { clearState, loadState, saveState } from './storage'; + +describe('utils/storage.ts', () => { + it('should load the state from localstorage - existing', () => { + jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValueOnce( + JSON.stringify({ + auth: { token: '123-456' }, + settings: { appearance: 'DARK' }, + }) + ); + const result = loadState(); + expect(result.accounts.token).toBe('123-456'); + expect(result.settings.appearance).toBe('DARK'); + }); + + it('should load the state from localstorage - empty', () => { + jest + .spyOn(localStorage.__proto__, 'getItem') + .mockReturnValueOnce(JSON.stringify({})); + const result = loadState(); + expect(result.accounts).toBeUndefined(); + expect(result.settings).toBeUndefined(); + }); + + it('should save the state to localstorage', () => { + jest.spyOn(localStorage.__proto__, 'setItem'); + saveState( + { + token: '123-456', + enterpriseAccounts: [], + }, + mockSettings + ); + expect(localStorage.setItem).toHaveBeenCalledTimes(1); + }); + + it('should clear the state from localstorage', () => { + jest.spyOn(localStorage.__proto__, 'clear'); + clearState(); + expect(localStorage.clear).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 000000000..b65d89f07 --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,23 @@ +import { AuthState, SettingsState } from '../types'; +import { Constants } from './constants'; + +export const loadState = (): { + accounts?: AuthState; + settings?: SettingsState; +} => { + const existing = localStorage.getItem(Constants.STORAGE_KEY); + const { auth: accounts, settings } = (existing && JSON.parse(existing)) || {}; + return { accounts, settings }; +}; + +export const saveState = ( + accounts: AuthState, + settings: SettingsState +): void => { + const settingsString = JSON.stringify({ auth: accounts, settings }); + localStorage.setItem(Constants.STORAGE_KEY, settingsString); +}; + +export const clearState = (): void => { + localStorage.clear(); +}; diff --git a/tsconfig.json b/tsconfig.json index 7bf1647a5..1dbc5dddd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,10 +6,18 @@ "outDir": "./build/", "sourceMap": true, "noImplicitAny": false, + "noUnusedLocals": true, "jsx": "react", "allowJs": true, - "typeRoots": ["./src/types", "node_modules/@types"] + "allowSyntheticDefaultImports": true, + "esModuleInterop": true }, "exclude": ["node_modules"], - "include": ["src/**/*.js", "src/**/*.ts", "src/**/*.tsx", "first-run.js", "main.js"] + "include": [ + "src/**/*.js", + "src/**/*.ts", + "src/**/*.tsx", + "first-run.js", + "main.js" + ] } diff --git a/webpack.common.js b/webpack.common.js index bf5b66b7f..94dd07b18 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -3,7 +3,7 @@ const webpack = require('webpack'); module.exports = { mode: 'development', - entry: './src/js/index.tsx', + entry: './src/index.tsx', devtool: 'inline-source-map', target: 'electron-renderer', module: { diff --git a/yarn.lock b/yarn.lock index 18a7d5824..060306d61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,6 +14,13 @@ dependencies: "@babel/highlight" "^7.10.3" +"@babel/code-frame@^7.10.4": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/core@^7.1.0", "@babel/core@^7.7.5": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.3.tgz#73b0e8ddeec1e3fdd7a2de587a60e17c440ec77e" @@ -136,6 +143,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz#60d9847f98c4cea1b279e005fdb7c28be5412d15" integrity sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw== +"@babel/helper-validator-identifier@^7.10.4": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" + integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== + "@babel/helpers@^7.10.1": version "7.10.1" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.1.tgz#a6827b7cb975c9d9cef5fd61d919f60d8844a973" @@ -154,6 +166,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.10.3": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.3.tgz#7e71d892b0d6e7d04a1af4c3c79d72c1f10f5315" @@ -251,13 +272,20 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.1", "@babel/template@^7.10.3", "@babel/template@^7.3.3": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.3.tgz#4d13bc8e30bf95b0ce9d175d30306f42a2c9a7b8" @@ -512,16 +540,6 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^25.5.0": - version "25.5.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d" - integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^1.1.1" - "@types/yargs" "^15.0.0" - chalk "^3.0.0" - "@jest/types@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" @@ -533,10 +551,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@primer/octicons-react@^11.1.0": - version "11.1.0" - resolved "https://registry.yarnpkg.com/@primer/octicons-react/-/octicons-react-11.1.0.tgz#1f03bfdc598ea24006cf03a0adcab2812d0fc958" - integrity sha512-1d/0HM4eR6+meZh3OR1HaVCg6DLBwrBEwQBN6SblOlA/98HRGaphRHGwUV7YMbSfFUc1029Ggx54ceKYHYwdXA== +"@primer/octicons-react@^11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@primer/octicons-react/-/octicons-react-11.2.0.tgz#af60301257cdbc1561c2647678282d3bce5a6c54" + integrity sha512-pi3DT5fUtATw2atMRQ7xCUUbdauA/hYTKfM2ijf3rtipeyQ7Ou3ij/JLhnc9Ke110ljHJh69j/PiINbbKAYegQ== "@scarf/scarf@^1.0.5": version "1.0.6" @@ -569,23 +587,40 @@ dependencies: defer-to-connect "^1.0.1" -"@testing-library/dom@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.17.0.tgz#42a359c21ba1cacdccf14d215a1a844011bb4e11" - integrity sha512-GT8cRigyD9Qr6+ECQHFTzhgX3srdDqD05I47CtXUp97gkFQ2lu7ylJbcxjHm25PMZyxooOk03bY2jVj2PdoNGg== +"@testing-library/dom@^7.28.1": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.29.0.tgz#60b18065bab50a5cde21fe80275a47a43024d9cc" + integrity sha512-0hhuJSmw/zLc6ewR9cVm84TehuTd7tbqBX9pRNSp8znJ9gTmSgesdbiGZtt8R6dL+2rgaPFp9Yjr7IU1HWm49w== dependencies: - "@babel/runtime" "^7.10.2" - aria-query "^4.2.1" - dom-accessibility-api "^0.4.5" - pretty-format "^25.5.0" + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^4.2.2" + chalk "^4.1.0" + dom-accessibility-api "^0.5.4" + lz-string "^1.4.4" + pretty-format "^26.6.2" + +"@testing-library/react-hooks@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.7.0.tgz#6d75c5255ef49bce39b6465bf6b49e2dac84919e" + integrity sha512-TwfbY6BWtWIHitjT05sbllyLIProcysC0dF0q1bbDa7OHLC6A6rJOYJwZ13hzfz3O4RtOuInmprBozJRyyo7/g== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/testing-library__react-hooks" "^3.4.0" -"@testing-library/react@^10.0.2": - version "10.4.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.0.tgz#d95302ddd256bc3ebd0b99c445bb236e4d9bbf36" - integrity sha512-koZCPOzH5fFXN3MPeQx6iT9o47U5y7zpyiEZlG3xP+XSApdHQfsx/PrsmTiPfrEjU/1DSaX75arjMcdkbnT04A== +"@testing-library/react@^11.2.2": + version "11.2.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.2.tgz#099c6c195140ff069211143cb31c0f8337bdb7b7" + integrity sha512-jaxm0hwUjv+hzC+UFEywic7buDC9JQ1q3cDsrWVSDAPmLotfA6E6kUHlYm/zOeGCac6g48DR36tFHxl7Zb+N5A== dependencies: - "@babel/runtime" "^7.10.3" - "@testing-library/dom" "^7.17.0" + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^7.28.1" + +"@types/aria-query@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" + integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== "@types/babel__core@^7.0.0": version "7.1.12" @@ -683,13 +718,10 @@ dependencies: "@types/node" "*" -"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" +"@types/history@*": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" @@ -703,14 +735,6 @@ dependencies: "@types/istanbul-lib-coverage" "*" -"@types/istanbul-reports@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz#e875cc689e47bce549ec81f3df5e6f6f11cfaeb2" - integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw== - dependencies: - "@types/istanbul-lib-coverage" "*" - "@types/istanbul-lib-report" "*" - "@types/istanbul-reports@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" @@ -718,7 +742,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@26.x", "@types/jest@^26.0.15": +"@types/jest@26.x": version "26.0.15" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.15.tgz#12e02c0372ad0548e07b9f4e19132b834cb1effe" integrity sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog== @@ -726,6 +750,14 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" +"@types/jest@^26.0.19": + version "26.0.19" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.19.tgz#e6fa1e3def5842ec85045bd5210e9bb8289de790" + integrity sha512-jqHoirTG61fee6v6rwbnEuKhpSKih0tuhqeFbCmMmErhtu3BYlOZaXWjffgOstMM4S/3iQD31lI5bGLTrs97yQ== + dependencies: + jest-diff "^26.0.0" + pretty-format "^26.0.0" + "@types/json-schema@*", "@types/json-schema@^7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" @@ -746,10 +778,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.47.tgz#5007b8866a2f9150de82335ca7e24dd1d59bdfb5" integrity sha512-yzBInQFhdY8kaZmqoL2+3U5dSTMrKaYcb561VU+lDzAYvqt+2lojvBEy+hmpSNuXnPTx7m9+04CzWYOUqWME2A== -"@types/node@^14.14.9": - version "14.14.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.9.tgz#04afc9a25c6ff93da14deabd65dc44485b53c8d6" - integrity sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw== +"@types/node@^14.14.14": + version "14.14.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae" + integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -771,24 +803,31 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== -"@types/react-native@*": - version "0.62.13" - resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.62.13.tgz#c688c5ae03e426f927f7e1fa1a59cd067f35d1c2" - integrity sha512-hs4/tSABhcJx+J8pZhVoXHrOQD89WFmbs8QiDLNSA9zNrD46pityAuBWuwk1aMjPk9I3vC5ewkJroVRHgRIfdg== +"@types/react-router-dom@^5.1.6": + version "5.1.6" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.6.tgz#07b14e7ab1893a837c8565634960dc398564b1fb" + integrity sha512-gjrxYqxz37zWEdMVvQtWPFMFj1dRDb4TGOcgyOfSXTrEXdF92L00WE3C471O3TV/RF1oskcStkXsOU0Ete4s/g== dependencies: + "@types/history" "*" "@types/react" "*" + "@types/react-router" "*" -"@types/react-redux@^7.1.7": - version "7.1.9" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.9.tgz#280c13565c9f13ceb727ec21e767abe0e9b4aec3" - integrity sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w== +"@types/react-router@*": + version "5.1.8" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.8.tgz#4614e5ba7559657438e17766bb95ef6ed6acc3fa" + integrity sha512-HzOyJb+wFmyEhyfp4D4NYrumi+LQgQL/68HvJO+q6XtuHSDvw6Aqov7sCAhjbNq3bUPgPqbdvjXC5HeB2oEAPg== dependencies: - "@types/hoist-non-react-statics" "^3.3.0" + "@types/history" "*" "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" -"@types/react-transition-group@^4.2.4": +"@types/react-test-renderer@*": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.0.tgz#9be47b375eeb906fced37049e67284a438d56620" + integrity sha512-nvw+F81OmyzpyIE1S0xWpLonLUZCMewslPuA8BtjSKc5XEbn8zEQBXS7KuOLHTNnSOEM2Pum50gHOoZ62tqTRg== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w== @@ -813,15 +852,12 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== -"@types/styled-components@^5.0.1": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.0.tgz#24d3412ba5395aa06e14fbc93c52f9454cebd0d6" - integrity sha512-ZFlLCuwF5r+4Vb7JUmd+Yr2S0UBdBGmI7ctFTgJMypIp3xOHI4LCFVn2dKMvpk6xDB2hLRykrEWMBwJEpUAUIQ== +"@types/testing-library__react-hooks@^3.4.0": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.4.1.tgz#b8d7311c6c1f7db3103e94095fe901f8fef6e433" + integrity sha512-G4JdzEcq61fUyV6wVW9ebHWEiLK2iQvaBuCHHn9eMSbZzVh4Z4wHnUGIvQOYCCYeu5DnUtFyNYuAAgbSaO/43Q== dependencies: - "@types/hoist-non-react-statics" "*" - "@types/react" "*" - "@types/react-native" "*" - csstype "^2.2.0" + "@types/react-test-renderer" "*" "@types/yargs-parser@*": version "15.0.0" @@ -835,149 +871,149 @@ dependencies: "@types/yargs-parser" "*" -"@webassemblyjs/ast@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" - integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== - dependencies: - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - -"@webassemblyjs/floating-point-hex-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" - integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== - -"@webassemblyjs/helper-api-error@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" - integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== - -"@webassemblyjs/helper-buffer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" - integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== - -"@webassemblyjs/helper-code-frame@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" - integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== - dependencies: - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/helper-fsm@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" - integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== - -"@webassemblyjs/helper-module-context@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" - integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== - dependencies: - "@webassemblyjs/ast" "1.9.0" - -"@webassemblyjs/helper-wasm-bytecode@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" - integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== - -"@webassemblyjs/helper-wasm-section@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" - integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - -"@webassemblyjs/ieee754@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" - integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== +"@webassemblyjs/ast@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.1.tgz#76c6937716d68bf1484c15139f5ed30b9abc8bb4" + integrity sha512-uMu1nCWn2Wxyy126LlGqRVlhdTOsO/bsBRI4dNq3+6SiSuRKRQX6ejjKgh82LoGAPSq72lDUiQ4FWVaf0PecYw== + dependencies: + "@webassemblyjs/helper-module-context" "1.9.1" + "@webassemblyjs/helper-wasm-bytecode" "1.9.1" + "@webassemblyjs/wast-parser" "1.9.1" + +"@webassemblyjs/floating-point-hex-parser@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.1.tgz#9eb0ff90a1cdeef51f36ba533ed9f06b5cdadd09" + integrity sha512-5VEKu024RySmLKTTBl9q1eO/2K5jk9ZS+2HXDBLA9s9p5IjkaXxWiDb/+b7wSQp6FRdLaH1IVGIfOex58Na2pg== + +"@webassemblyjs/helper-api-error@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.1.tgz#ad89015c4246cd7f5ed0556700237f8b9c2c752f" + integrity sha512-y1lGmfm38djrScwpeL37rRR9f1D6sM8RhMpvM7CYLzOlHVboouZokXK/G88BpzW0NQBSvCCOnW5BFhten4FPfA== + +"@webassemblyjs/helper-buffer@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.1.tgz#186e67ac25f9546ea7939759413987f157524133" + integrity sha512-uS6VSgieHbk/m4GSkMU5cqe/5TekdCzQso4revCIEQ3vpGZgqSSExi4jWpTWwDpAHOIAb1Jfrs0gUB9AA4n71w== + +"@webassemblyjs/helper-code-frame@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.1.tgz#aab177b7cc87a318a8f8664ad68e2c3828ebc42b" + integrity sha512-ZQ2ZT6Evk4DPIfD+92AraGYaFIqGm4U20e7FpXwl7WUo2Pn1mZ1v8VGH8i+Y++IQpxPbQo/UyG0Khs7eInskzA== + dependencies: + "@webassemblyjs/wast-printer" "1.9.1" + +"@webassemblyjs/helper-fsm@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.1.tgz#527e91628e84d13d3573884b3dc4c53a81dcb911" + integrity sha512-J32HGpveEqqcKFS0YbgicB0zAlpfIxJa5MjxDxhu3i5ltPcVfY5EPvKQ1suRguFPehxiUs+/hfkwPEXom/l0lw== + +"@webassemblyjs/helper-module-context@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.1.tgz#778670b3d471f7cf093d1e7c0dde431b54310e16" + integrity sha512-IEH2cMmEQKt7fqelLWB5e/cMdZXf2rST1JIrzWmf4XBt3QTxGdnnLvV4DYoN8pJjOx0VYXsWg+yF16MmJtolZg== + dependencies: + "@webassemblyjs/ast" "1.9.1" + +"@webassemblyjs/helper-wasm-bytecode@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.1.tgz#563f59bcf409ccf469edde168b9426961ffbf6df" + integrity sha512-i2rGTBqFUcSXxyjt2K4vm/3kkHwyzG6o427iCjcIKjOqpWH8SEem+xe82jUk1iydJO250/CvE5o7hzNAMZf0dQ== + +"@webassemblyjs/helper-wasm-section@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.1.tgz#f7988f94c12b01b99a16120cb01dc099b00e4798" + integrity sha512-FetqzjtXZr2d57IECK+aId3D0IcGweeM0CbAnJHkYJkcRTHP+YcMb7Wmc0j21h5UWBpwYGb9dSkK/93SRCTrGg== + dependencies: + "@webassemblyjs/ast" "1.9.1" + "@webassemblyjs/helper-buffer" "1.9.1" + "@webassemblyjs/helper-wasm-bytecode" "1.9.1" + "@webassemblyjs/wasm-gen" "1.9.1" + +"@webassemblyjs/ieee754@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.1.tgz#3b715871ca7d75784717cf9ceca9d7b81374b8af" + integrity sha512-EvTG9M78zP1MmkBpUjGQHZc26DzPGZSLIPxYHCjQsBMo60Qy2W34qf8z0exRDtxBbRIoiKa5dFyWer/7r1aaSQ== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" - integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== +"@webassemblyjs/leb128@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.1.tgz#b2ecaa39f9e8277cc9c707c1ca8b2aa7b27d0b72" + integrity sha512-Oc04ub0vFfLnF+2/+ki3AE+anmW4sv9uNBqb+79fgTaPv6xJsOT0dhphNfL3FrME84CbX/D1T9XT8tjFo0IIiw== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" - integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== - -"@webassemblyjs/wasm-edit@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" - integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/helper-wasm-section" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-opt" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/wasm-gen@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" - integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wasm-opt@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" - integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - -"@webassemblyjs/wasm-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" - integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wast-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" - integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/floating-point-hex-parser" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-code-frame" "1.9.0" - "@webassemblyjs/helper-fsm" "1.9.0" +"@webassemblyjs/utf8@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.1.tgz#d02d9daab85cda3211e43caf31dca74c260a73b0" + integrity sha512-llkYtppagjCodFjo0alWOUhAkfOiQPQDIc5oA6C9sFAXz7vC9QhZf/f8ijQIX+A9ToM3c9Pq85X0EX7nx9gVhg== + +"@webassemblyjs/wasm-edit@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.1.tgz#e27a6bdbf78e5c72fa812a2fc3cbaad7c3e37578" + integrity sha512-S2IaD6+x9B2Xi8BCT0eGsrXXd8UxAh2LVJpg1ZMtHXnrDcsTtIX2bDjHi40Hio6Lc62dWHmKdvksI+MClCYbbw== + dependencies: + "@webassemblyjs/ast" "1.9.1" + "@webassemblyjs/helper-buffer" "1.9.1" + "@webassemblyjs/helper-wasm-bytecode" "1.9.1" + "@webassemblyjs/helper-wasm-section" "1.9.1" + "@webassemblyjs/wasm-gen" "1.9.1" + "@webassemblyjs/wasm-opt" "1.9.1" + "@webassemblyjs/wasm-parser" "1.9.1" + "@webassemblyjs/wast-printer" "1.9.1" + +"@webassemblyjs/wasm-gen@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.1.tgz#56a0787d1fa7994fdc7bea59004e5bec7189c5fc" + integrity sha512-bqWI0S4lBQsEN5FTZ35vYzfKUJvtjNnBobB1agCALH30xNk1LToZ7Z8eiaR/Z5iVECTlBndoRQV3F6mbEqE/fg== + dependencies: + "@webassemblyjs/ast" "1.9.1" + "@webassemblyjs/helper-wasm-bytecode" "1.9.1" + "@webassemblyjs/ieee754" "1.9.1" + "@webassemblyjs/leb128" "1.9.1" + "@webassemblyjs/utf8" "1.9.1" + +"@webassemblyjs/wasm-opt@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.1.tgz#fbdf8943a825e6dcc4cd69c3e092289fa4aec96c" + integrity sha512-gSf7I7YWVXZ5c6XqTEqkZjVs8K1kc1k57vsB6KBQscSagDNbAdxt6MwuJoMjsE1yWY1tsuL+pga268A6u+Fdkg== + dependencies: + "@webassemblyjs/ast" "1.9.1" + "@webassemblyjs/helper-buffer" "1.9.1" + "@webassemblyjs/wasm-gen" "1.9.1" + "@webassemblyjs/wasm-parser" "1.9.1" + +"@webassemblyjs/wasm-parser@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.1.tgz#5e8352a246d3f605312c8e414f7990de55aaedfa" + integrity sha512-ImM4N2T1MEIond0MyE3rXvStVxEmivQrDKf/ggfh5pP6EHu3lL/YTAoSrR7shrbKNPpeKpGesW1LIK/L4kqduw== + dependencies: + "@webassemblyjs/ast" "1.9.1" + "@webassemblyjs/helper-api-error" "1.9.1" + "@webassemblyjs/helper-wasm-bytecode" "1.9.1" + "@webassemblyjs/ieee754" "1.9.1" + "@webassemblyjs/leb128" "1.9.1" + "@webassemblyjs/utf8" "1.9.1" + +"@webassemblyjs/wast-parser@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.1.tgz#e25ef13585c060073c1db0d6bd94340fdeee7596" + integrity sha512-2xVxejXSvj3ls/o2TR/zI6p28qsGupjHhnHL6URULQRcXmryn3w7G83jQMcT7PHqUfyle65fZtWLukfdLdE7qw== + dependencies: + "@webassemblyjs/ast" "1.9.1" + "@webassemblyjs/floating-point-hex-parser" "1.9.1" + "@webassemblyjs/helper-api-error" "1.9.1" + "@webassemblyjs/helper-code-frame" "1.9.1" + "@webassemblyjs/helper-fsm" "1.9.1" "@xtuc/long" "4.2.2" -"@webassemblyjs/wast-printer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" - integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== +"@webassemblyjs/wast-printer@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.1.tgz#b9f38e93652037d4f3f9c91584635af4191ed7c1" + integrity sha512-tDV8V15wm7mmbAH6XvQRU1X+oPGmeOzYsd6h7hlRLz6QpV4Ec/KKxM8OpLtFmQPLCreGxTp+HuxtH4pRIZyL9w== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" + "@webassemblyjs/ast" "1.9.1" + "@webassemblyjs/wast-parser" "1.9.1" "@xtuc/long" "4.2.2" "@webpack-cli/info@^1.1.0": @@ -1169,7 +1205,7 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aria-query@^4.2.1: +aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== @@ -1266,10 +1302,10 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== -axios@=0.21.0: - version "0.21.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca" - integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw== +axios@=0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== dependencies: follow-redirects "^1.10.0" @@ -2088,10 +2124,10 @@ dmg-builder@22.9.1: js-yaml "^3.14.0" sanitize-filename "^1.6.3" -dom-accessibility-api@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.4.5.tgz#d9c1cefa89f509d8cf132ab5d250004d755e76e3" - integrity sha512-HcPDilI95nKztbVikaN2vzwvmv0sE8Y2ZJFODy/m15n7mGXLeOKGiys9qWVbFbh+aq/KYj2lqMLybBOkYAEXqg== +dom-accessibility-api@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166" + integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ== dom-helpers@^5.0.1: version "5.1.4" @@ -2556,13 +2592,12 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -final-form@^4.19.1: - version "4.20.0" - resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.20.0.tgz#454ba46f783a4c4404ad875cf36f470395ad5efa" - integrity sha512-kdPGNlR/23M2p7ccVwE/vCBQH9TH1NAhhMVkETHbaQXkTWIJdEii3ZdHrOgYvFY7O87myEhcqzx3zjMERtoNJg== +final-form@^4.20.1: + version "4.20.1" + resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.20.1.tgz#525a7f7f27f55c28d8994b157b24d6104fc560e9" + integrity sha512-IIsOK3JRxJrN72OBj7vFWZxtGt3xc1bYwJVPchjVWmDol9DlzMSAOPB+vwe75TUYsw1JaH0fTQnIgwSQZQ9Acg== dependencies: "@babel/runtime" "^7.10.0" - "@scarf/scarf" "^1.0.5" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" @@ -2572,6 +2607,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + follow-redirects@^1.10.0: version "1.13.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" @@ -2847,7 +2890,7 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.1.0: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -3848,47 +3891,24 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash.get@^4.1.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= -lodash.isfunction@^3.0.7, lodash.isfunction@^3.0.8: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" - integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== - -lodash.isobject@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" - integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.merge@^4.3.1: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.reduce@^4.3.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" - integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= - -lodash.set@^4.0.0: +lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= @@ -3903,11 +3923,6 @@ lodash.toarray@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= -lodash.unset@^4.1.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed" - integrity sha1-Nw0dPoW3Kn4bDN8tJyEhMG8j5O0= - lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -3942,6 +3957,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -4146,14 +4166,14 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -nock@^12.0.3: - version "12.0.3" - resolved "https://registry.yarnpkg.com/nock/-/nock-12.0.3.tgz#83f25076dbc4c9aa82b5cdf54c9604c7a778d1c9" - integrity sha512-QNb/j8kbFnKCiyqi9C5DD0jH/FubFGj5rt9NQFONXwQm3IPB0CULECg/eS3AU1KgZb/6SwUa4/DTRKhVxkGABw== +nock@^13.0.5: + version "13.0.5" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.5.tgz#a618c6f86372cb79fac04ca9a2d1e4baccdb2414" + integrity sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" - lodash "^4.17.13" + lodash.set "^4.3.2" propagate "^2.0.0" node-emoji@^1.8.1: @@ -4359,6 +4379,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -4472,6 +4499,13 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-dir@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" + integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== + dependencies: + find-up "^5.0.0" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -4598,20 +4632,10 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@=2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.0.tgz#8a03c7777883b29b37fb2c4348c66a78e980418b" - integrity sha512-yYerpkvseM4iKD/BXLYUkQV5aKt4tQPqaGW6EsZjzyu0r7sVZZNPJW4Y8MyKmicp6t42XUPcBVA+H6sB3gqndw== - -pretty-format@^25.5.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" - integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== - dependencies: - "@jest/types" "^25.5.0" - ansi-regex "^5.0.0" - ansi-styles "^4.0.0" - react-is "^16.12.0" +prettier@=2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== pretty-format@^26.0.0, pretty-format@^26.6.2: version "26.6.2" @@ -4646,7 +4670,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.4" -prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.6.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -4751,7 +4775,7 @@ react-final-form@^6.4.0: "@scarf/scarf" "^1.0.5" ts-essentials "^6.0.5" -react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -4761,18 +4785,7 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -react-redux@=7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" - integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA== - dependencies: - "@babel/runtime" "^7.5.5" - hoist-non-react-statics "^3.3.0" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^16.9.0" - -react-router-dom@^5.1.2: +react-router-dom@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== @@ -4811,7 +4824,7 @@ react-test-renderer@=16.13.1: react-is "^16.8.6" scheduler "^0.19.1" -react-transition-group@^4.3.0: +react-transition-group@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== @@ -4900,74 +4913,6 @@ reduce-flatten@^2.0.0: resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== -reduce-reducers@^0.1.0: - version "0.1.5" - resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.5.tgz#ff77ca8068ff41007319b8b4b91533c7e0e54576" - integrity sha512-uoVmQnZQ+BtKKDKpBdbBri5SLNyIK9ULZGOA504++VbHcwouWE+fJDIo8AuESPF9/EYSkI0v05LDEQK6stCbTA== - -redux-actions@^0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/redux-actions/-/redux-actions-0.10.1.tgz#bb442ee37dd9643a94933e4071e089f435587135" - integrity sha1-u0Qu433ZZDqUkz5AceCJ9DVYcTU= - dependencies: - reduce-reducers "^0.1.0" - -redux-mock-store@=1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872" - integrity sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA== - dependencies: - lodash.isplainobject "^4.0.6" - -redux-storage-decorator-filter@=1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/redux-storage-decorator-filter/-/redux-storage-decorator-filter-1.1.8.tgz#c0b7b5563b8ba138ce79c03ad54a2878a3a53ee5" - integrity sha1-wLe1VjuLoTjOecA61UooeKOlPuU= - dependencies: - lodash.get "^4.1.2" - lodash.isfunction "^3.0.7" - lodash.isobject "^3.0.2" - lodash.reduce "^4.3.0" - lodash.set "^4.0.0" - lodash.unset "^4.1.0" - -redux-storage-engine-localstorage@=1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/redux-storage-engine-localstorage/-/redux-storage-engine-localstorage-1.1.4.tgz#2849278d78970f0c3f5f3d4727caa3c30783ed49" - integrity sha1-KEknjXiXDww/Xz1HJ8qjwweD7Uk= - -redux-storage-merger-simple@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/redux-storage-merger-simple/-/redux-storage-merger-simple-1.0.5.tgz#29a2886b0e770d9b70811aca800aa8efae89fb73" - integrity sha1-KaKIaw53DZtwgRrKgAqo766J+3M= - dependencies: - lodash.isobject "^3.0.2" - lodash.merge "^4.3.1" - -redux-storage@=4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/redux-storage/-/redux-storage-4.1.2.tgz#e06f4bdeee262aead9132fc9f7eadc67e9f9bea2" - integrity sha1-4G9L3u4mKurZEy/J9+rcZ+n5vqI= - dependencies: - lodash.isfunction "^3.0.8" - lodash.isobject "^3.0.2" - loose-envify "^1.2.0" - redux-actions "^0.10.1" - redux-storage-merger-simple "^1.0.2" - -redux-thunk@=2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" - integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== - -redux@=4.0.5, redux@^4.0.0: - version "4.0.5" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" - integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== - dependencies: - loose-envify "^1.4.0" - symbol-observable "^1.2.0" - regenerator-runtime@^0.13.4: version "0.13.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" @@ -5595,11 +5540,6 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" -symbol-observable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -5651,6 +5591,11 @@ tapable@^2.0.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.1.1.tgz#b01cc1902d42a7bb30514e320ce21c456f72fd3f" integrity sha512-Wib1S8m2wdpLbmQz0RBEVosIyvb/ykfKXf3ZIDqvWoMg/zTNm6G/tDSuUM61J1kNCDXWJrLHGSFeMhAG+gAGpQ== +tapable@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" + integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== + temp-file@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.3.7.tgz#686885d635f872748e384e871855958470aeb18a" @@ -5817,10 +5762,10 @@ ts-jest@^26.4.4: semver "7.x" yargs-parser "20.x" -ts-loader@^8.0.11: - version "8.0.11" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.11.tgz#35d58a65932caacb120426eea59eca841786c899" - integrity sha512-06X+mWA2JXoXJHYAesUUL4mHFYhnmyoCdQVMXofXF552Lzd4wNwSGg7unJpttqUP7ziaruM8d7u8LUB6I1sgzA== +ts-loader@^8.0.12: + version "8.0.12" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.12.tgz#1de9f1de65176318c1e6d187bfc496182f8dc2a0" + integrity sha512-UIivVfGVJDdwwjgSrbtcL9Nf10c1BWnL1mxAQUVcnhNIn/P9W3nP5v60Z0aBMtc7ZrE11lMmU6+5jSgAXmGaYw== dependencies: chalk "^2.3.0" enhanced-resolve "^4.0.0" @@ -5894,10 +5839,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.2.tgz#6369ef22516fe5e10304aae5a5c4862db55380e9" - integrity sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ== +typescript@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== typical@^5.0.0, typical@^5.2.0: version "5.2.0" @@ -6108,10 +6053,10 @@ webpack-merge@^4.2.2: dependencies: lodash "^4.17.15" -webpack-merge@^5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.4.0.tgz#81bef0a7d23fc1e6c24b06ad8bf22ddeb533a3a3" - integrity sha512-/scBgu8LVPlHDgqH95Aw1xS+L+PHrpHKOwYVGFaNOQl4Q4wwwWDarwB1WdZAbLQ24SKhY3Awe7VZGYAdp+N+gQ== +webpack-merge@^5.7.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.7.3.tgz#2a0754e1877a25a8bbab3d2475ca70a052708213" + integrity sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA== dependencies: clone-deep "^4.0.1" wildcard "^2.0.0" @@ -6124,17 +6069,17 @@ webpack-sources@^2.1.1: source-list-map "^2.0.1" source-map "^0.6.1" -webpack@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.6.0.tgz#282d10434c403b070ed91d459b385e873b51a07d" - integrity sha512-SIeFuBhuheKElRbd84O35UhKc0nxlgSwtzm2ksZ0BVhRJqxVJxEguT/pYhfiR0le/pxTa1VsCp7EOYyTsa6XOA== +webpack@^5.11.0: + version "5.11.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.11.0.tgz#1647abc060441d86d01d8835b8f0fc1dae2fe76f" + integrity sha512-ubWv7iP54RqAC/VjixgpnLLogCFbAfSOREcSWnnOlZEU8GICC5eKmJSu6YEnph2N2amKqY9rvxSwgyHxVqpaRw== dependencies: "@types/eslint-scope" "^3.7.0" "@types/estree" "^0.0.45" - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" + "@webassemblyjs/ast" "1.9.1" + "@webassemblyjs/helper-module-context" "1.9.1" + "@webassemblyjs/wasm-edit" "1.9.1" + "@webassemblyjs/wasm-parser" "1.9.1" acorn "^8.0.4" browserslist "^4.14.5" chrome-trace-event "^1.0.2" @@ -6147,9 +6092,9 @@ webpack@^5.6.0: loader-runner "^4.1.0" mime-types "^2.1.27" neo-async "^2.6.2" - pkg-dir "^4.2.0" + pkg-dir "^5.0.0" schema-utils "^3.0.0" - tapable "^2.0.0" + tapable "^2.1.1" terser-webpack-plugin "^5.0.3" watchpack "^2.0.0" webpack-sources "^2.1.1"