├── .github ├── banner.png ├── dark.png └── light.png ├── .gitignore ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── appveyor.yml ├── build ├── icon.icns └── icon.ico ├── gulpfile.js ├── index.html ├── package.json ├── resources └── audio │ ├── hark-error.mp3 │ ├── hark-new-items.mp3 │ └── hark-success.mp3 ├── src ├── fonts │ └── themify │ │ ├── themify.eot │ │ ├── themify.svg │ │ ├── themify.ttf │ │ └── themify.woff ├── js │ ├── Actions │ │ ├── Accounts.ts │ │ ├── App.ts │ │ ├── AppAlerts.ts │ │ ├── Authentication.ts │ │ ├── HawkEye.ts │ │ ├── NotificationFilters.ts │ │ ├── Notifications.ts │ │ ├── Repositories.ts │ │ ├── RepositoryMuteFilters.ts │ │ ├── Settings.ts │ │ ├── Setup.ts │ │ └── UIActions │ │ │ ├── Accounts.ts │ │ │ ├── App.ts │ │ │ ├── AppAlerts.ts │ │ │ ├── Notifications.ts │ │ │ ├── RepositoryMuteFilters.ts │ │ │ └── Settings.ts │ ├── App.ts │ ├── Config │ │ └── HawkEye.default.ts │ ├── Constants │ │ ├── Actions │ │ │ ├── Accounts.ts │ │ │ ├── App.ts │ │ │ ├── AppAlerts.ts │ │ │ ├── Authentication.ts │ │ │ ├── Index.ts │ │ │ ├── NotificationFilter.ts │ │ │ ├── Notifications.ts │ │ │ ├── Repositories.ts │ │ │ ├── RepositoryMuteFilters.ts │ │ │ ├── Settings.ts │ │ │ └── Setup.ts │ │ ├── Lang │ │ │ └── Date.ts │ │ ├── Models │ │ │ ├── AppAlert.ts │ │ │ ├── NotificationFilterSet.ts │ │ │ └── Settings.ts │ │ ├── Resources │ │ │ └── Sound.ts │ │ ├── Services │ │ │ └── GitHub.ts │ │ ├── State │ │ │ └── RepositoryMuteFilters.ts │ │ └── System │ │ │ └── Electron.ts │ ├── Core │ │ ├── InstanceCache.ts │ │ ├── Interfaces │ │ │ ├── IInstanceCache.ts │ │ │ ├── IRequest.ts │ │ │ ├── IRequestFactory.ts │ │ │ ├── IRouting.ts │ │ │ └── IStoreCreator.ts │ │ ├── Renderer.tsx │ │ ├── Request.ts │ │ ├── RequestFactory.ts │ │ ├── Routing.ts │ │ └── StoreCreator.ts │ ├── Electron │ │ ├── Dialogs │ │ │ └── Accounts.ts │ │ ├── Menus │ │ │ ├── Accounts.ts │ │ │ ├── Notification.ts │ │ │ └── Repository.ts │ │ ├── OAuthBrowserWindow.ts │ │ └── Tasks │ │ │ ├── App.ts │ │ │ ├── Notification.ts │ │ │ └── Settings.ts │ ├── Filter │ │ ├── Filter.ts │ │ ├── FilterFunctions │ │ │ └── GitHubNotifications │ │ │ │ ├── Index.ts │ │ │ │ ├── Read.ts │ │ │ │ ├── Reason.ts │ │ │ │ ├── Repository.ts │ │ │ │ └── Subject.ts │ │ └── Interfaces │ │ │ ├── IFilter.ts │ │ │ └── IFilterFunction.ts │ ├── GitHub │ │ ├── Activity.ts │ │ ├── Authentication.ts │ │ ├── GitHub.ts │ │ ├── Interfaces │ │ │ ├── IGitHub.ts │ │ │ ├── IGitHubActivity.ts │ │ │ ├── IGitHubAuthentication.ts │ │ │ └── IGitHubUsers.ts │ │ └── Users.ts │ ├── HawkEye.ts │ ├── Helpers │ │ ├── Lang │ │ │ ├── Array.ts │ │ │ ├── Audio.ts │ │ │ ├── Date.ts │ │ │ ├── Sort.ts │ │ │ ├── String.ts │ │ │ └── Timeout.ts │ │ ├── Models │ │ │ ├── Accounts.ts │ │ │ ├── App.ts │ │ │ ├── AppAlert.ts │ │ │ ├── GitHubNotification.ts │ │ │ ├── GitHubNotificationFilterSet.ts │ │ │ ├── GitHubRepository.ts │ │ │ ├── GitHubUser.ts │ │ │ ├── RepositoryMuteFilters.ts │ │ │ └── Settings.ts │ │ ├── Services │ │ │ └── GitHub.ts │ │ ├── State │ │ │ ├── Reducify.ts │ │ │ └── Store.ts │ │ └── System │ │ │ ├── Electron.ts │ │ │ ├── Environment.ts │ │ │ └── Scheduler.ts │ ├── Interfaces │ │ ├── HawkEye │ │ │ └── IHawkEyeConfig.ts │ │ ├── IState │ │ │ ├── IState.ts │ │ │ ├── IStateAccounts.ts │ │ │ ├── IStateApp.ts │ │ │ ├── IStateAppAlerts.ts │ │ │ ├── IStateAuthentication.ts │ │ │ ├── IStateNotificationFilters.ts │ │ │ ├── IStateNotifications.ts │ │ │ ├── IStateRepositories.ts │ │ │ ├── IStateRepositoryMuteFilters.ts │ │ │ ├── IStateSettings.ts │ │ │ └── IStateSetup.ts │ │ └── Models │ │ │ ├── IAppAlert.ts │ │ │ ├── IGitHubNotification.ts │ │ │ ├── IGitHubNotificationFilterSet.ts │ │ │ ├── IGitHubRepository.ts │ │ │ ├── IGitHubUser.ts │ │ │ └── INotificationFilterSet.ts │ ├── Main.ts │ ├── Reducers │ │ ├── Accounts.ts │ │ ├── App.ts │ │ ├── AppAlerts.ts │ │ ├── Authentication.ts │ │ ├── Index.ts │ │ ├── NotificationFilters.ts │ │ ├── Notifications.ts │ │ ├── Repositories.ts │ │ ├── RepositoryMuteFilters.ts │ │ ├── Settings.ts │ │ └── Setup.ts │ ├── Routes.ts │ ├── Scheduler │ │ ├── Interfaces │ │ │ ├── IScheduler.ts │ │ │ └── ISchedulerJobs.ts │ │ ├── NodeSchedule.ts │ │ └── Scheduler.ts │ ├── Services │ │ ├── GitHubAccountsService.ts │ │ ├── GitHubAuthenticationService.ts │ │ ├── GitHubNotificationsService.ts │ │ └── Interfaces │ │ │ ├── IGitHubAccountsService.ts │ │ │ ├── IGitHubAuthenticationService.ts │ │ │ └── IGitHubNotificationsService.ts │ └── View │ │ ├── App.tsx │ │ ├── Components │ │ ├── AppAlerts │ │ │ ├── AppAlert.tsx │ │ │ └── Index.tsx │ │ ├── AppBar │ │ │ ├── Account.tsx │ │ │ └── Index.tsx │ │ ├── GenericError.tsx │ │ ├── NoAccounts │ │ │ └── Index.tsx │ │ ├── NoNotifications │ │ │ └── Index.tsx │ │ ├── NotificationFilters │ │ │ ├── Index.tsx │ │ │ ├── NotificationFilterRepositoryFilter.tsx │ │ │ └── NotificationFilterStringFilter.tsx │ │ ├── NotificationsList │ │ │ └── Index.tsx │ │ └── ViewBar │ │ │ └── Index.tsx │ │ ├── Container.tsx │ │ ├── Index.tsx │ │ ├── Settings │ │ ├── Accounts │ │ │ ├── RepositoryMuteFilters.tsx │ │ │ └── View.tsx │ │ ├── Index.tsx │ │ └── Sections │ │ │ ├── Accounts.tsx │ │ │ ├── App.tsx │ │ │ ├── Frequency.tsx │ │ │ ├── Notifications.tsx │ │ │ └── Sounds.tsx │ │ └── Ui │ │ ├── Anchor.tsx │ │ ├── AppLoading.tsx │ │ ├── Btn.tsx │ │ ├── BtnTo.tsx │ │ ├── Button.tsx │ │ ├── CenteredBox.tsx │ │ ├── Icon.tsx │ │ ├── Index.ts │ │ ├── Loader.tsx │ │ ├── Notification.tsx │ │ ├── ProfilePicture.tsx │ │ ├── RoundedBtnSet.tsx │ │ ├── Scroll.tsx │ │ └── Toggle.tsx └── scss │ ├── _variables.scss │ ├── base │ ├── _fonts.scss │ ├── _form.scss │ ├── _images.scss │ ├── _lists.scss │ ├── _page.scss │ └── _typography.scss │ ├── components │ ├── _app-alert.scss │ ├── _app.scss │ ├── _btn.scss │ ├── _loader.scss │ ├── _no-outline.scss │ ├── _notification.scss │ ├── _profile-picture.scss │ ├── _toggle.scss │ └── _view-bar.scss │ ├── directives │ ├── _breakpoints.scss │ ├── _font-sizes.scss │ ├── _grid.scss │ ├── _headings.scss │ ├── _keyframes.scss │ ├── _pushes.scss │ ├── _softs.scss │ └── _transparents.scss │ ├── generic │ ├── _app-drag.scss │ ├── _backgrounds.scss │ ├── _borders.scss │ ├── _clearfix.scss │ ├── _displays.scss │ ├── _floats.scss │ ├── _hards.scss │ ├── _heights.scss │ ├── _highers.scss │ ├── _line-heights.scss │ ├── _max-widths.scss │ ├── _min-heights.scss │ ├── _n-tops.scss │ ├── _overflows.scss │ ├── _positionings.scss │ ├── _pushes.scss │ ├── _round.scss │ ├── _softs.scss │ ├── _text-aligns.scss │ ├── _text-colors.scss │ ├── _text-emboss.scss │ ├── _text-fonts.scss │ ├── _text-sizes.scss │ ├── _text-transforms.scss │ ├── _text-weights.scss │ ├── _tops.scss │ └── _widths.scss │ ├── hawkeye.scss │ ├── objects │ ├── _cover-height.scss │ ├── _dialog.scss │ ├── _dropdown.scss │ ├── _fixed-overlay.scss │ ├── _flexi-side.scss │ ├── _grid.scss │ ├── _hard-bottom.scss │ ├── _hard-left.scss │ ├── _hard-right.scss │ ├── _hard-top.scss │ ├── _hideable-left.scss │ ├── _img-scale.scss │ ├── _loading-overlay.scss │ ├── _media.scss │ ├── _truncate.scss │ └── _vscroll.scss │ ├── themes │ ├── _dark.scss │ └── _light.scss │ └── vendors │ ├── _themify.scss │ └── _virtualized.scss ├── tsconfig.json ├── tsconfig.main.json ├── typings.json ├── webpack.config.js └── webpack.dev.config.js /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/.github/banner.png -------------------------------------------------------------------------------- /.github/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/.github/dark.png -------------------------------------------------------------------------------- /.github/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/.github/light.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | typings/* 4 | !typings/custom 5 | 6 | main.js 7 | app.js 8 | hawkeye.css 9 | 10 | src/js/Config/HawkEye.ts 11 | 12 | /*.woff 13 | /*.eot 14 | /*.svg 15 | /*.ttf 16 | 17 | npm-debug.log 18 | 19 | dist -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Two spaces for tabs. 3 | "editor.tabSize": 2, 4 | "editor.insertSpaces": true, 5 | 6 | // Ruler at 80 chars 7 | "editor.rulers" : [80], 8 | 9 | // Show whitespaces in the editor pane. 10 | "editor.renderWhitespace": true, 11 | 12 | // When saving, automatically trim whitespace 13 | "files.trimTrailingWhitespace" : true, 14 | 15 | // Exclude folders from being shown in VSC. Just to 16 | // clean the view up a little bit. 17 | "files.exclude": { 18 | "dist" : true, 19 | "node_modules" : true 20 | }, 21 | 22 | // Exclude folders from being shown in the search 23 | "search.exclude": { 24 | "**/node_modules": true 25 | }, 26 | 27 | "typescript.tsdk": "./node_modules/typescript/lib" 28 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Hawk Eye 2 | 3 | Please search issues and pull requests before adding something new to avoid duplicating 4 | efforts and conversations. 5 | 6 | ## Development setup 7 | 8 | - Fork the [Hawk Eye](https://github.com/harksys/hawkeye) repository in to your own account, and clone to your device. 9 | - Install the project dependencies. 10 | 11 | ```bash 12 | $ npm install 13 | ``` 14 | 15 | - Install typing definitions. 16 | 17 | ```bash 18 | $ typings Install 19 | ``` 20 | 21 | - You'll need to setup an OAuth GitHub app and add your configuration file under `src/js/Config/HawkEye.ts`. Follow the `HawkEye.default.ts` file for more information. 22 | - Make your changes and build, then run the app. 23 | 24 | ```bash 25 | $ gulp 26 | $ electron . 27 | ``` 28 | 29 | - You can package the app using, and run from the dist folder. 30 | 31 | ```bash 32 | $ npm run pack 33 | ``` 34 | 35 | - Confirm your changes work, and create a Pull Request to the Hawk Eye repository. 36 | 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2017 Hark Systems LTD. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./.github/banner.png?raw=true) 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/3468o1as6k2jm73u?svg=true)](https://ci.appveyor.com/project/andrewhathaway/hawkeye) 4 | 5 | 6 | To read about Hawk Eye, visit the [Hark website](https://harksys.com/labs/introducing-hawk-eye-a-useful-app-for-github-notifications). 7 | 8 | ## Features 9 | 10 | Hawk Eye has many features, including: 11 | 12 | - Scheduled notification pulling 13 | - Filter by type, repository and more 14 | - Flexible per-repository mute filters 15 | - Mark single and multiple notifications as read 16 | - Support for multiple accounts 17 | - Light and dark mode 18 | - Responsive design 19 | 20 | ## Usage 21 | 22 | To get started and try out Hawk Eye, download the latest release for your platform on the [Releases Page](https://github.com/harksys/hawkeye/releases). You'll then be able to auto-update when new updates are available. Note: this currently only works for macOS, Windows incoming. 23 | 24 | ## Screenshots 25 | 26 | ### Dark Mode 27 | 28 | ![](./.github/dark.png?raw=true) 29 | 30 | ### Light Mode 31 | 32 | ![](./.github/light.png?raw=true) 33 | 34 | ## Contributing 35 | 36 | For information regarding contributing to this project, please read the [Contributing](./CONTRIBUTING.md) document. 37 | 38 | ## License 39 | 40 | [MIT License](./LICENSE.md) 41 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.2.{build} 2 | 3 | platform: 4 | - x64 5 | 6 | cache: 7 | - node_modules 8 | - app\node_modules 9 | - '%APPDATA%\npm-cache' 10 | - '%USERPROFILE%\.electron' 11 | 12 | init: 13 | - git config --global core.autocrlf input 14 | 15 | install: 16 | - ps: Install-Product node 6 x64 17 | - git reset --hard HEAD 18 | - npm install npm -g 19 | - npm install electron-builder@next # force install next version to test electron-builder 20 | - npm install 21 | - npm prune 22 | 23 | build_script: 24 | - node --version 25 | - npm --version 26 | - npm run citask 27 | 28 | test: off -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/build/icon.ico -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const del = require('del'); 2 | const gulp = require('gulp'); 3 | const rename = require('gulp-rename'); 4 | const runSeq = require('run-sequence'); 5 | const typescript = require('gulp-typescript'); 6 | const webpack = require('webpack-stream'); 7 | const sass = require('gulp-sass'); 8 | 9 | const ENV = process.env.NODE_ENV || 'production'; 10 | 11 | /** 12 | * Clean Task 13 | * 14 | * Clean up old build. Nice and clean. 15 | */ 16 | gulp.task('clean', () => del(['./app.js', './main.js'])); 17 | 18 | /** 19 | * Main Task 20 | * 21 | * Build the main file for Electron 22 | */ 23 | gulp.task('main', () => 24 | { 25 | var tsProject = typescript.createProject('tsconfig.main.json'); 26 | var res = gulp.src('src/js/Main.ts') 27 | .pipe(tsProject()); 28 | 29 | return res.js 30 | .pipe(rename('main.js')) 31 | .pipe(gulp.dest('./')); 32 | }); 33 | 34 | /** 35 | * Bundle Task 36 | * 37 | * Bundle our application in to a single file. 38 | */ 39 | gulp.task('bundle', () => 40 | { 41 | var webpackConfig = ENV === 'production' 42 | ? './webpack.config.js' 43 | : './webpack.dev.config.js'; 44 | 45 | return gulp.src('./src/js/App.ts') 46 | .pipe(webpack(require(webpackConfig))) 47 | .pipe(gulp.dest('./')); 48 | }); 49 | 50 | /* 51 | * Styles Task 52 | * 53 | * Use Node Sass to build our main stylesheet 54 | */ 55 | gulp.task('styles', () => 56 | { 57 | return gulp.src('./src/scss/hawkeye.scss') 58 | .pipe(sass({ 59 | outputStyle : 'compressed', 60 | includePaths : [ 61 | './node_modules/compass-mixins/lib' 62 | ] 63 | }).on('err', sass.logError)) 64 | .pipe(gulp.dest('./')); 65 | }); 66 | 67 | /* 68 | * Default Config Task 69 | * 70 | * This is for CI builds to test a project can build 71 | */ 72 | gulp.task('configdefault', () => 73 | { 74 | return gulp.src('./src/js/Config/HawkEye.default.ts') 75 | .pipe(rename('HawkEye.ts')) 76 | .pipe(gulp.dest('./src/js/Config')); 77 | }); 78 | 79 | /** 80 | * Default Task 81 | * 82 | * Do all the things. 83 | */ 84 | gulp.task('default', cb => runSeq('clean', 'main', 'bundle', 'styles', cb)); 85 | 86 | /** 87 | * Watch Task 88 | * 89 | * Watch for changes 90 | */ 91 | gulp.task('watch', () => 92 | { 93 | gulp.watch('./src/js/**/*', {}, () => gulp.start('default')); 94 | gulp.watch('./src/scss/**/*.scss', {}, () => gulp.start('styles')); 95 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HawkEye 6 | 7 | 8 | 9 |
10 | 20 | 21 | -------------------------------------------------------------------------------- /resources/audio/hark-error.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/resources/audio/hark-error.mp3 -------------------------------------------------------------------------------- /resources/audio/hark-new-items.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/resources/audio/hark-new-items.mp3 -------------------------------------------------------------------------------- /resources/audio/hark-success.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/resources/audio/hark-success.mp3 -------------------------------------------------------------------------------- /src/fonts/themify/themify.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/src/fonts/themify/themify.eot -------------------------------------------------------------------------------- /src/fonts/themify/themify.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/src/fonts/themify/themify.ttf -------------------------------------------------------------------------------- /src/fonts/themify/themify.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harksys/HawkEye/77203dc40a14300887c58505707ae33955819b6d/src/fonts/themify/themify.woff -------------------------------------------------------------------------------- /src/js/Actions/App.ts: -------------------------------------------------------------------------------- 1 | import ActionConstants from 'Constants/Actions/Index'; 2 | 3 | export function setCurrentAccountId(currentAccountId: number) 4 | { 5 | return { 6 | type : ActionConstants.app.SET_CURRENT_ACCOUNT_ID, 7 | currentAccountId 8 | }; 9 | }; 10 | 11 | export function setIsPolling(isPolling: boolean) 12 | { 13 | return { 14 | type : ActionConstants.app.SET_APP_IS_POLLING, 15 | isPolling 16 | }; 17 | }; 18 | 19 | export function setLastPoll(lastPoll: string) 20 | { 21 | return { 22 | type : ActionConstants.app.SET_APP_LAST_POLL, 23 | lastPoll 24 | }; 25 | }; -------------------------------------------------------------------------------- /src/js/Actions/Authentication.ts: -------------------------------------------------------------------------------- 1 | import ActionConstants from 'Constants/Actions/Index'; 2 | 3 | export function setIsAuthenticating(isAuthenticating: boolean) 4 | { 5 | return { 6 | type : ActionConstants.authentication.SET_IS_AUTHENTICATING, 7 | isAuthenticating 8 | }; 9 | }; -------------------------------------------------------------------------------- /src/js/Actions/HawkEye.ts: -------------------------------------------------------------------------------- 1 | import { getAccountIds } from 'Helpers/Models/Accounts'; 2 | import { wait } from 'Helpers/Lang/Timeout'; 3 | import { getCurrentPollPeriod } from 'Helpers/Models/Settings'; 4 | 5 | import { 6 | setSetupIsLoading, 7 | setSetupRenderApp, 8 | setSetupShowLoading 9 | } from 'Actions/Setup'; 10 | import { updateAccounts } from 'Actions/Accounts'; 11 | 12 | import { 13 | pollingMethod, 14 | configurePollingScheduler 15 | } from 'Helpers/System/Scheduler'; 16 | 17 | /** 18 | * @todo: lol more callback hell...fix this. 19 | */ 20 | export function appSetupFlow() 21 | { 22 | return dispatch => 23 | { 24 | /* 25 | * Render the application, and wait some time 26 | * for completion 27 | */ 28 | dispatch(setSetupRenderApp(true)); 29 | wait(250) 30 | .then(() => 31 | { 32 | /* 33 | * Update the accounts we have stored. 34 | */ 35 | dispatch(updateAccounts(() => 36 | { 37 | pollingMethod(getAccountIds()); 38 | 39 | /* 40 | * Setup our polling scheduler 41 | */ 42 | configurePollingScheduler(getCurrentPollPeriod()); 43 | 44 | /* 45 | * Hide the loading page, wait for it to fade out. 46 | * When it has, remove the loading screen from the DOM. 47 | */ 48 | dispatch(setSetupShowLoading(false)); 49 | wait(250) 50 | .then(() => dispatch(setSetupIsLoading(false))); 51 | })); 52 | }); 53 | }; 54 | }; -------------------------------------------------------------------------------- /src/js/Actions/NotificationFilters.ts: -------------------------------------------------------------------------------- 1 | import ActionConstants from 'Constants/Actions/Index'; 2 | 3 | export function clearFilters(accountId: number) 4 | { 5 | return { 6 | type : ActionConstants.notificationFilter.CLEAR_FILTERS, 7 | accountId 8 | }; 9 | }; 10 | 11 | /** 12 | * @param {number} accountId 13 | * @param {boolean} read 14 | */ 15 | export function setReadFilter(accountId: number, read: boolean) 16 | { 17 | return { 18 | type : ActionConstants.notificationFilter.SET_READ_FILTER, 19 | accountId, 20 | read 21 | }; 22 | }; 23 | 24 | /** 25 | * @param {string} subjectType 26 | */ 27 | export function addSubjectTypeFilter(accountId: number, subjectType: string) 28 | { 29 | return addFilter(accountId, 'subjectType', subjectType); 30 | }; 31 | 32 | /** 33 | * @param {string} subjectType 34 | */ 35 | export function removeSubjectTypeFilter(accountId: number, subjectType: string) 36 | { 37 | return removeFilter(accountId, 'subjectType', subjectType); 38 | }; 39 | 40 | /** 41 | * @param {string} reasonType 42 | */ 43 | export function addReasonFilter(accountId: number, reasonType: string) 44 | { 45 | return addFilter(accountId, 'reasonType', reasonType); 46 | }; 47 | 48 | /** 49 | * @param {string} reasonType 50 | */ 51 | export function removeReasonFilter(accountId: number, reasonType: string) 52 | { 53 | return removeFilter(accountId, 'reasonType', reasonType); 54 | }; 55 | 56 | /** 57 | * @param {string|number} id 58 | */ 59 | export function addRepositoryFilter(accountId: number, id: string | number) 60 | { 61 | return addFilter(accountId, 'repository', id); 62 | }; 63 | 64 | /** 65 | * @param {string|number} id 66 | */ 67 | export function removeRepositoryFilter(accountId: number, id: string | number) 68 | { 69 | return removeFilter(accountId, 'repository', id); 70 | }; 71 | 72 | /** 73 | * @param {string} area 74 | * @param {string|number} filter 75 | */ 76 | export function addFilter(accountId: number, area: string, filter: string | number) 77 | { 78 | return { 79 | type : ActionConstants.notificationFilter.ADD_FILTER, 80 | accountId, 81 | area, 82 | filter, 83 | }; 84 | } 85 | 86 | /** 87 | * @param {string} area 88 | * @param {string|number} filter 89 | */ 90 | export function removeFilter(accountId: number, area: string, filter: string | number) 91 | { 92 | return { 93 | type : ActionConstants.notificationFilter.REMOVE_FILTER, 94 | accountId, 95 | area, 96 | filter 97 | }; 98 | } -------------------------------------------------------------------------------- /src/js/Actions/Repositories.ts: -------------------------------------------------------------------------------- 1 | import ActionConstants from 'Constants/Actions/Index'; 2 | 3 | /** 4 | * @param {IGitHubRepository} repository 5 | */ 6 | export function addRepository(repository: IGitHubRepository) 7 | { 8 | return { 9 | type : ActionConstants.repositories.ADD_REPOSITORY, 10 | repository 11 | }; 12 | }; -------------------------------------------------------------------------------- /src/js/Actions/RepositoryMuteFilters.ts: -------------------------------------------------------------------------------- 1 | import ActionConstants from 'Constants/Actions/Index'; 2 | 3 | export function setupRepositoryMuteFilter(accountId: number, repositoryId: string) 4 | { 5 | return { 6 | type : ActionConstants.repositoryMuteFilters.SETUP_FILTER, 7 | accountId, 8 | repositoryId 9 | }; 10 | }; 11 | 12 | export function setReasonFilter(accountId: number, 13 | repoId: string, 14 | filterName: string, 15 | enabled: boolean) 16 | { 17 | return { 18 | type : ActionConstants.repositoryMuteFilters.SET_REASON_FILTER, 19 | accountId, 20 | repoId, 21 | filterName, 22 | enabled 23 | }; 24 | }; 25 | 26 | export function setSubjectFilter(accountId: number, 27 | repoId: string, 28 | filterName: string, 29 | enabled: boolean) 30 | { 31 | return { 32 | type : ActionConstants.repositoryMuteFilters.SET_SUBJECT_FILTER, 33 | accountId, 34 | repoId, 35 | filterName, 36 | enabled 37 | }; 38 | }; 39 | 40 | export function removeFilter(accountId: number, repoId: string) 41 | { 42 | return { 43 | type : ActionConstants.repositoryMuteFilters.REMOVE_FILTER, 44 | accountId, 45 | repoId 46 | }; 47 | }; -------------------------------------------------------------------------------- /src/js/Actions/Settings.ts: -------------------------------------------------------------------------------- 1 | import ActionConstants from 'Constants/Actions/Index'; 2 | 3 | /** 4 | * @param {string} period 5 | */ 6 | export function updatePollPeriod(period: string) 7 | { 8 | return dispatch => dispatch(updateSettingsValue('pollPeriod', period)); 9 | }; 10 | 11 | /** 12 | * @param {string} key 13 | * @param {any} value 14 | */ 15 | export function updateSettingsValue(key: string, value: any) 16 | { 17 | return { 18 | type : ActionConstants.settings.SET_SETTINGS_VALUE, 19 | key, 20 | value 21 | }; 22 | }; 23 | 24 | /** 25 | * @param {boolean} enabled 26 | */ 27 | export function setNewItemsEnabled(enabled: boolean) 28 | { 29 | return updateSoundSettingsEnabled('newItemsEnabled', enabled); 30 | }; 31 | 32 | /** 33 | * @param {boolean} enabled 34 | */ 35 | export function setAlertSuccessEnabled(enabled: boolean) 36 | { 37 | return updateSoundSettingsEnabled('alertSuccessEnabled', enabled); 38 | }; 39 | 40 | /** 41 | * @param {boolean} enabled 42 | */ 43 | export function setAlertErrorEnabled(enabled: boolean) 44 | { 45 | return updateSoundSettingsEnabled('alertErrorEnabled', enabled); 46 | }; 47 | 48 | 49 | /** 50 | * @param {string} key 51 | * @param {boolean} enabled 52 | */ 53 | export function updateSoundSettingsEnabled(key: string, enabled: boolean) 54 | { 55 | return { 56 | type : ActionConstants.settings.SET_SOUND_SETTINGS_ENABLED, 57 | key, 58 | enabled 59 | }; 60 | }; 61 | 62 | /** 63 | * @param {string} action 64 | */ 65 | export function setNotificationDoubleClickAction(action: string) 66 | { 67 | return { 68 | type : ActionConstants.settings.SET_NOTIFICATIONS_DOUBLE_CLICK_ACTION, 69 | action 70 | }; 71 | }; 72 | 73 | /** 74 | * @param {boolean} confirm 75 | */ 76 | export function setConfirmBeforeMarkingMultipleNotificationsAsRead(confirm: boolean) 77 | { 78 | return { 79 | type : ActionConstants.settings.SET_CONFIRM_BEFORE_MARKING_NOTIFICATIONS_AS_READ, 80 | confirm 81 | }; 82 | }; 83 | 84 | /** 85 | * @param {string} colorMode 86 | */ 87 | export function setColorMode(colorMode: string) 88 | { 89 | return { 90 | type : ActionConstants.settings.SET_COLOR_MODE, 91 | colorMode 92 | }; 93 | }; -------------------------------------------------------------------------------- /src/js/Actions/Setup.ts: -------------------------------------------------------------------------------- 1 | import ActionConstants from 'Constants/Actions/Index'; 2 | 3 | export function setSetupIsLoading(isLoading: boolean) 4 | { 5 | return { 6 | type : ActionConstants.setup.SET_SETUP_IS_LOADING, 7 | isLoading 8 | }; 9 | }; 10 | 11 | export function setSetupShowLoading(showLoading: boolean) 12 | { 13 | return { 14 | type : ActionConstants.setup.SET_SETUP_SHOW_LOADING, 15 | showLoading 16 | }; 17 | }; 18 | 19 | export function setSetupRenderApp(renderApp: boolean) 20 | { 21 | return { 22 | type : ActionConstants.setup.SET_SETUP_RENDER_APP, 23 | renderApp 24 | }; 25 | }; -------------------------------------------------------------------------------- /src/js/Actions/UIActions/App.ts: -------------------------------------------------------------------------------- 1 | import { replace } from 'react-router-redux'; 2 | import { setCurrentAccountId } from 'Actions/App'; 3 | 4 | /** 5 | * @param {number} accountId 6 | */ 7 | export function switchAccount(accountId: number) 8 | { 9 | return dispatch => 10 | { 11 | dispatch(setCurrentAccountId(accountId)); 12 | dispatch(replace('/')); 13 | }; 14 | }; -------------------------------------------------------------------------------- /src/js/Actions/UIActions/AppAlerts.ts: -------------------------------------------------------------------------------- 1 | import { updateActions } from 'Constants/System/Electron'; 2 | import { autoUpdateQuitAndInstall } from 'Electron/Tasks/App'; 3 | 4 | /** 5 | * @param {string} actionName 6 | * @param {any={}} actionParams 7 | */ 8 | export function handleAppAlertActionClick(actionName: string, actionParams: any = {}) 9 | { 10 | return dispatch => 11 | { 12 | if (actionName !== updateActions.AU_QUIT_INSTALL) { 13 | return; 14 | } 15 | 16 | autoUpdateQuitAndInstall(); 17 | }; 18 | }; -------------------------------------------------------------------------------- /src/js/Actions/UIActions/Notifications.ts: -------------------------------------------------------------------------------- 1 | import { notificationDoubleClickActions } from 'Constants/Models/Settings'; 2 | 3 | import { 4 | getElectron, 5 | openExternalUrl, 6 | getCurrentWindow, 7 | copyStringToClipboard 8 | } from 'Helpers/System/Electron'; 9 | import { getNotificationWebUrl } from 'Helpers/Services/GitHub'; 10 | import { 11 | getNotificationDoubleClickAction, 12 | shouldConfirmBeforeMarkingNotificationsAsRead 13 | } from 'Helpers/Models/Settings'; 14 | import { markNotificationsAsRead } from 'Helpers/Models/GitHubNotification'; 15 | 16 | import { clearFilters } from 'Actions/NotificationFilters'; 17 | 18 | /** 19 | * @param {number} accountId 20 | * @param {IGitHubNotification} notification 21 | */ 22 | export function doubleClickNotification(accountId: number, notification: IGitHubNotification) 23 | { 24 | return dispatch => 25 | { 26 | let action = getNotificationDoubleClickAction(); 27 | if (action === notificationDoubleClickActions.nothing) { 28 | return; 29 | } 30 | 31 | let url = getNotificationWebUrl(notification); 32 | if (action === notificationDoubleClickActions.copyLink) { 33 | copyStringToClipboard(url); 34 | return; 35 | } 36 | 37 | openExternalUrl(url); 38 | }; 39 | }; 40 | 41 | /** 42 | * @param {number} accountId 43 | * @param {IGitHubNotification[]} notifications 44 | */ 45 | export function markMultipleNotificationsAsRead(accountId: number, notifications: IGitHubNotification[]) 46 | { 47 | return dispatch => 48 | { 49 | /* 50 | * If we shouldn't confirm, then just go ahead and clear notificatins 51 | */ 52 | if (!shouldConfirmBeforeMarkingNotificationsAsRead()) { 53 | dispatch(clearFilters(accountId)); 54 | markNotificationsAsRead(accountId, notifications); 55 | return; 56 | } 57 | 58 | /* 59 | * Ask the user whether we should really go through with this 60 | */ 61 | getElectron().remote.dialog.showMessageBox(getCurrentWindow(), { 62 | type : 'question', 63 | title : 'Mark all as read?', 64 | message : 'Are you sure you want to mark all these notifications as read?', 65 | buttons : [ 66 | 'Yes', 67 | 'No' 68 | ] 69 | }, index => 70 | { 71 | /* 72 | * Answered No, so don't! 73 | */ 74 | if (index === 1) { 75 | return; 76 | } 77 | 78 | /* 79 | * Answered yes, so lets go ahead 80 | */ 81 | dispatch(clearFilters(accountId)); 82 | markNotificationsAsRead(accountId, notifications); 83 | }); 84 | }; 85 | }; -------------------------------------------------------------------------------- /src/js/Actions/UIActions/Settings.ts: -------------------------------------------------------------------------------- 1 | import InstanceCache from 'Core/InstanceCache'; 2 | 3 | import { updatePollPeriod } from 'Actions/Settings'; 4 | import { configurePollingScheduler } from 'Helpers/System/Scheduler'; 5 | 6 | /** 7 | * @param {string} pollPeriod 8 | */ 9 | export function configurePollPeriod(pollPeriod: string) 10 | { 11 | let scheduler = InstanceCache.getInstance('IScheduler'); 12 | 13 | return dispatch => 14 | { 15 | dispatch(updatePollPeriod(pollPeriod)); 16 | configurePollingScheduler(pollPeriod); 17 | }; 18 | }; -------------------------------------------------------------------------------- /src/js/App.ts: -------------------------------------------------------------------------------- 1 | import HawkEye from './HawkEye'; 2 | 3 | // ES6 Promise Polyfill 4 | import * as FakePromise from 'es6-promise'; 5 | (FakePromise as any).polyfill(); 6 | 7 | window.onload = () => 8 | { 9 | new HawkEye(); 10 | } -------------------------------------------------------------------------------- /src/js/Config/HawkEye.default.ts: -------------------------------------------------------------------------------- 1 | import { gitHubScopes } from 'Constants/Services/GitHub'; 2 | 3 | const config: IHawkEyeConfig = { 4 | github : { 5 | clientId : '', 6 | clientSecret : '', 7 | scopes : [ 8 | gitHubScopes.notifications 9 | ], 10 | webUrl : 'https://github.com/' 11 | }, 12 | appAlerts : { 13 | showFor : 4000 14 | } 15 | }; 16 | 17 | export default config; -------------------------------------------------------------------------------- /src/js/Constants/Actions/Accounts.ts: -------------------------------------------------------------------------------- 1 | 2 | const AccountsActions = { 3 | ADD_ACCOUNT : 'ADD_ACCOUNT', 4 | REMOVE_ACCOUNT : 'REMOVE_ACCOUNT' 5 | }; 6 | 7 | export default AccountsActions; -------------------------------------------------------------------------------- /src/js/Constants/Actions/App.ts: -------------------------------------------------------------------------------- 1 | 2 | const AppActions = { 3 | SET_CURRENT_ACCOUNT_ID : 'SET_CURRENT_ACCOUNT_ID', 4 | SET_APP_IS_POLLING : 'SET_APP_IS_POLLING', 5 | SET_APP_LAST_POLL : 'SET_APP_LAST_POLL' 6 | }; 7 | 8 | export default AppActions; -------------------------------------------------------------------------------- /src/js/Constants/Actions/AppAlerts.ts: -------------------------------------------------------------------------------- 1 | 2 | const AppAlertsActions = { 3 | ADD_APP_ALERT : 'ADD_APP_ALERT', 4 | REMOVE_APP_ALERT : 'REMOVE_APP_ALERT', 5 | SHOW_APP_ALERT : 'SHOW_APP_ALERT', 6 | HIDE_APP_ALERT : 'HIDE_APP_ALERT', 7 | HIDE_ALL_APP_ALERTS : 'HIDE_ALL_APP_ALERTS' 8 | }; 9 | 10 | export default AppAlertsActions; -------------------------------------------------------------------------------- /src/js/Constants/Actions/Authentication.ts: -------------------------------------------------------------------------------- 1 | 2 | const AuthenticationActions = { 3 | SET_IS_AUTHENTICATING : 'SET_IS_AUTHENTICATING' 4 | }; 5 | 6 | export default AuthenticationActions; 7 | -------------------------------------------------------------------------------- /src/js/Constants/Actions/Index.ts: -------------------------------------------------------------------------------- 1 | import AccountsActions from './Accounts'; 2 | import AppActions from './App'; 3 | import NotificationsActions from './Notifications'; 4 | import SettingsActions from './Settings'; 5 | import AuthenticationActions from './Authentication'; 6 | import AppAlertsActions from './AppAlerts'; 7 | import SetupActions from './Setup'; 8 | import NotificationFilterActions from './NotificationFilter'; 9 | import RepositoryActions from './Repositories'; 10 | import RepositoryMuteFilterActions from './RepositoryMuteFilters'; 11 | 12 | const ActionConstants = { 13 | accounts : AccountsActions, 14 | app : AppActions, 15 | notifications : NotificationsActions, 16 | settings : SettingsActions, 17 | authentication : AuthenticationActions, 18 | appAlerts : AppAlertsActions, 19 | setup : SetupActions, 20 | notificationFilter : NotificationFilterActions, 21 | repositories : RepositoryActions, 22 | repositoryMuteFilters : RepositoryMuteFilterActions 23 | }; 24 | 25 | export default ActionConstants; -------------------------------------------------------------------------------- /src/js/Constants/Actions/NotificationFilter.ts: -------------------------------------------------------------------------------- 1 | 2 | const NotificationFilterActions = { 3 | ADD_FILTER : 'ADD_FILTER', 4 | REMOVE_FILTER : 'REMOVE_FILTER', 5 | SET_READ_FILTER : 'SET_READ_FILTER', 6 | CLEAR_FILTERS : 'CLEAR_FILTERS' 7 | }; 8 | 9 | export default NotificationFilterActions; -------------------------------------------------------------------------------- /src/js/Constants/Actions/Notifications.ts: -------------------------------------------------------------------------------- 1 | 2 | const NotificationsActions = { 3 | INGEST_NOTIFICATION : 'INGEST_NOTIFICATION', 4 | INGEST_NOTIFICATIONS : 'INGEST_NOTIFICATIONS', 5 | REMOVE_ACCOUNT_NOTIFICATIONS : 'REMOVE_ACCOUNT_NOTIFICATIONS', 6 | MARK_NOTIFICATION_AS_READ : 'MARK_NOTIFICATION_AS_READ' 7 | }; 8 | 9 | export default NotificationsActions; -------------------------------------------------------------------------------- /src/js/Constants/Actions/Repositories.ts: -------------------------------------------------------------------------------- 1 | 2 | const RepositoryActions = { 3 | ADD_REPOSITORY : 'ADD_REPOSITORY' 4 | }; 5 | 6 | export default RepositoryActions; -------------------------------------------------------------------------------- /src/js/Constants/Actions/RepositoryMuteFilters.ts: -------------------------------------------------------------------------------- 1 | 2 | const RepositoryMuteFilterActions = { 3 | SETUP_FILTER : 'SETUP_FILTER', 4 | SET_REASON_FILTER : 'SET_REASON_FILTER', 5 | SET_SUBJECT_FILTER : 'SET_SUBJECT_FILTER', 6 | REMOVE_FILTER : 'REMOVE_FILTER' 7 | }; 8 | 9 | export default RepositoryMuteFilterActions; -------------------------------------------------------------------------------- /src/js/Constants/Actions/Settings.ts: -------------------------------------------------------------------------------- 1 | 2 | const SettingsActions = { 3 | SET_SETTINGS_VALUE : 'SET_SETTINGS_VALUE', 4 | SET_SOUND_SETTINGS_ENABLED : 'SET_SOUND_SETTINGS_ENABLED', 5 | SET_NOTIFICATIONS_DOUBLE_CLICK_ACTION : 'SET_NOTIFICATIONS_DOUBLE_CLICK_ACTION', 6 | SET_CONFIRM_BEFORE_MARKING_NOTIFICATIONS_AS_READ : 'SET_CONFIRM_BEFORE_MARKING_NOTIFICATIONS_AS_READ', 7 | SET_COLOR_MODE : 'SET_COLOR_MODE' 8 | }; 9 | 10 | export default SettingsActions; -------------------------------------------------------------------------------- /src/js/Constants/Actions/Setup.ts: -------------------------------------------------------------------------------- 1 | 2 | const SetupActions = { 3 | SET_SETUP_IS_LOADING : 'SET_SETUP_IS_LOADING', 4 | SET_SETUP_SHOW_LOADING : 'SET_SETUP_SHOW_LOADING', 5 | SET_SETUP_RENDER_APP : 'SET_SETUP_RENDER_APP' 6 | } 7 | 8 | export default SetupActions; -------------------------------------------------------------------------------- /src/js/Constants/Lang/Date.ts: -------------------------------------------------------------------------------- 1 | 2 | export const cronPeriods = { 3 | fiveMinutes : '*/5 * * * *', 4 | fifteenMinute : '*/15 * * * *', 5 | thirtyMinute : '*/30 * * * *', 6 | fourtyFiveMinute : '*/45 * * * *', 7 | sixtyMinute : '* * /1 * * *' 8 | }; 9 | 10 | export const cronPeriodPrettyNames = { 11 | fiveMinutes : '5 Minutes', 12 | fifteenMinute : '15 Minutes', 13 | thirtyMinute : '30 Minutes', 14 | fourtyFiveMinute : '45 Minutes', 15 | sixtyMinute : '1 Hour', 16 | }; -------------------------------------------------------------------------------- /src/js/Constants/Models/AppAlert.ts: -------------------------------------------------------------------------------- 1 | 2 | export const appAlertStatuses = { 3 | success : 'SUCCESS', 4 | error : 'ERROR', 5 | warning : 'WARNING' 6 | }; -------------------------------------------------------------------------------- /src/js/Constants/Models/NotificationFilterSet.ts: -------------------------------------------------------------------------------- 1 | 2 | export const defaultNotificationFilterSet: INotificationFilterSet = { 3 | read : false, 4 | subjectType : [], 5 | reasonType : [], 6 | repository : [] 7 | }; -------------------------------------------------------------------------------- /src/js/Constants/Models/Settings.ts: -------------------------------------------------------------------------------- 1 | 2 | export const colorModes = { 3 | light : 'LIGHT', 4 | dark : 'DARK' 5 | }; 6 | 7 | export const notificationDoubleClickActions = { 8 | nothing : 'NOTHING', 9 | open : 'OPEN', 10 | copyLink : 'COPYLINK' 11 | }; 12 | 13 | export const defaultPollPeriod = 'fifteenMinute'; -------------------------------------------------------------------------------- /src/js/Constants/Resources/Sound.ts: -------------------------------------------------------------------------------- 1 | 2 | export const soundClipPaths = { 3 | harkSuccess : './resources/audio/hark-success.mp3', 4 | harkError : './resources/audio/hark-error.mp3', 5 | harkNewItems : './resources/audio/hark-new-items.mp3' 6 | }; -------------------------------------------------------------------------------- /src/js/Constants/Services/GitHub.ts: -------------------------------------------------------------------------------- 1 | 2 | export const gitHubApiUrl: string = 'https://api.github.com'; 3 | 4 | 5 | /* 6 | * GitHub Scope Strings 7 | * 8 | * Most, but cut down to never allow access to sensitive things. 9 | * For example, delete_repo etc. 10 | */ 11 | export const gitHubScopes = { 12 | user : 'user', 13 | userEmail : 'user:email', 14 | userFollow : 'user:follow', 15 | publicRepo : 'public_repo', 16 | repo : 'repo', 17 | repoDeployment : 'repo_deployment', 18 | repoStatus : 'repo:status', 19 | gist : 'gist', 20 | notifications : 'notifications' 21 | }; 22 | 23 | export const githubNotificationSubjectTypes = { 24 | Issue : 'ISSUE', 25 | PullRequest : 'PULLREQUEST', 26 | Commit : 'COMMIT', 27 | Release : 'RELEASE' 28 | }; 29 | 30 | export const githubNotificationSubjectTypeIcons = { 31 | ISSUE : 'issue-opened', 32 | PULLREQUEST : 'git-pull-request', 33 | COMMIT : 'git-commit', 34 | RELEASE : 'tag' 35 | }; 36 | 37 | export const gitHubNotificationSubjectTypePrettyNames = { 38 | ISSUE : 'Issue', 39 | PULLREQUEST : 'Pull Request', 40 | COMMIT : 'Commit', 41 | RELEASE : 'Release' 42 | }; 43 | 44 | export const gitHubNotificationReasonTypes = { 45 | subscribed : 'SUBSCRIBED', 46 | manual : 'MANUAL', 47 | author : 'AUTHOR', 48 | comment : 'COMMENT', 49 | mention : 'MENTION', 50 | team_mention : 'TEAMMENTION', 51 | state_change : 'STATECHANGE', 52 | assign : 'ASSIGN' 53 | }; 54 | 55 | export const gitHubNotificationReasonTypePrettyNames = { 56 | SUBSCRIBED : 'Watching', 57 | MANUAL : 'Watching Thread', 58 | AUTHOR : 'Authored', 59 | COMMENT : 'Commented', 60 | MENTION : 'Mentioned', 61 | TEAMMENTION : 'Team Mention', 62 | STATECHANGE : 'State Changed', 63 | ASSIGN : 'Assigned' 64 | }; -------------------------------------------------------------------------------- /src/js/Constants/State/RepositoryMuteFilters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | gitHubNotificationReasonTypes, 3 | githubNotificationSubjectTypes 4 | } from 'Constants/Services/GitHub'; 5 | 6 | export const defaultRepositoryMuteFilter: IStateRepositoryMuteFiltersAccountRepo = { 7 | allowedSubjectTypes : Object.keys(githubNotificationSubjectTypes), 8 | allowedReasons : Object.keys(gitHubNotificationReasonTypes) 9 | }; -------------------------------------------------------------------------------- /src/js/Constants/System/Electron.ts: -------------------------------------------------------------------------------- 1 | 2 | export const updateActions = { 3 | AU_QUIT_INSTALL : 'AU_QUIT_INSTALL', 4 | AU_UPDATE_AVAILABLE : 'AU_UPDATE_AVAILABLE' 5 | }; -------------------------------------------------------------------------------- /src/js/Core/InstanceCache.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | class InstanceCache 4 | { 5 | 6 | private classInstances: any = {}; 7 | 8 | constructor() 9 | { 10 | this.classInstances = {}; 11 | } 12 | 13 | /** 14 | * @param {string} className 15 | * @param {any} implementation 16 | * @returns InstanceCache 17 | */ 18 | public addInstance = (className: string, implementation: any, force: boolean = false): InstanceCache => 19 | { 20 | // Check the instance hasn't already been assigned, error if so. 21 | // Skip this check if force is true 22 | if (typeof this.classInstances[className] !== 'undefined' 23 | && !force) { 24 | throw new Error('InstanceCache: Instance for class "' 25 | + className 26 | + '" already exists in cache.'); 27 | } 28 | 29 | // Add the instances to the cache 30 | this.classInstances[className] = implementation; 31 | 32 | return this; 33 | } 34 | 35 | /** 36 | * @param {string} className 37 | * @returns T 38 | */ 39 | public getInstance = (className: string): T => 40 | { 41 | if (typeof this.classInstances[className] === 'undefined') { 42 | throw new Error('InstanceCache: Class "' 43 | + className 44 | + '" not found in InstanceCache.'); 45 | } 46 | 47 | return this.classInstances[className]; 48 | } 49 | 50 | } 51 | 52 | export default new InstanceCache(); -------------------------------------------------------------------------------- /src/js/Core/Interfaces/IInstanceCache.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IInstanceCache 3 | { 4 | 5 | addInstance(className: string, implementation: any): IInstanceCache; 6 | 7 | getInstance(className: string): T; 8 | 9 | }; -------------------------------------------------------------------------------- /src/js/Core/Interfaces/IRequest.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IRequest 3 | { 4 | 5 | setHost(host?: string): IRequest; 6 | 7 | setUrl(url?: string): IRequest; 8 | 9 | setMethod(method?: string): IRequest; 10 | 11 | setQuery(query?: any): IRequest; 12 | 13 | setBody(body?: any): IRequest; 14 | 15 | setHeaders(headers?: any): IRequest; 16 | 17 | execute(): Promise; 18 | 19 | sendAsJson(): IRequest; 20 | 21 | }; -------------------------------------------------------------------------------- /src/js/Core/Interfaces/IRequestFactory.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface IRequestFactory 4 | { 5 | 6 | getHost(): string; 7 | 8 | newRequest(): IRequest; 9 | 10 | }; -------------------------------------------------------------------------------- /src/js/Core/Interfaces/IRouting.ts: -------------------------------------------------------------------------------- 1 | interface IRouting 2 | { 3 | getHistory(): ReactRouter.History; 4 | 5 | getSyncdHistory(): ReactRouterRedux.ReactRouterReduxHistory; 6 | 7 | syncHistoryWithStore(store: Redux.Store): IRouting; 8 | 9 | getRoutes(): ReactRouter.PlainRoute; 10 | 11 | addRouteConfig(routeConfig: ReactRouter.PlainRoute): IRouting; 12 | 13 | addRouteConfigs(...routeConfigs: ReactRouter.PlainRoute[]): IRouting; 14 | }; -------------------------------------------------------------------------------- /src/js/Core/Interfaces/IStoreCreator.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IStoreCreator 3 | { 4 | getStore(): Redux.Store; 5 | }; -------------------------------------------------------------------------------- /src/js/Core/Renderer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import Container from 'View/Container'; 5 | 6 | export function renderApplication(containerElement: HTMLElement, 7 | storeCreator: IStoreCreator, 8 | routing: IRouting): Promise 9 | { 10 | /* 11 | * Render the application with our routes and store! 12 | */ 13 | return new Promise(resolve => 14 | { 15 | render( 16 | , 19 | containerElement, 20 | resolve 21 | ); 22 | }); 23 | }; -------------------------------------------------------------------------------- /src/js/Core/RequestFactory.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import Request from './Request'; 5 | 6 | class RequestFactory implements IRequestFactory 7 | { 8 | private host: string = ''; 9 | 10 | constructor(host: string) 11 | { 12 | this.host = host; 13 | } 14 | 15 | public getHost = (): string => 16 | { 17 | return this.host; 18 | }; 19 | 20 | public newRequest = (): Request => 21 | { 22 | return new Request(this.host); 23 | } 24 | }; 25 | 26 | export default RequestFactory; -------------------------------------------------------------------------------- /src/js/Core/Routing.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { hashHistory } from 'react-router'; 4 | import { syncHistoryWithStore } from 'react-router-redux'; 5 | 6 | class Routing implements IRouting 7 | { 8 | public routes: ReactRouter.PlainRoute; 9 | 10 | public historyInstance: ReactRouter.History; 11 | 12 | public syncdHistory: ReactRouterRedux.ReactRouterReduxHistory; 13 | 14 | public childRoutes: ReactRouter.PlainRoute[]; 15 | 16 | /** 17 | * @param {any} appContainer 18 | */ 19 | constructor(appContainer: any, indexComponent: any) 20 | { 21 | /* 22 | * Setup the default chilRoutes and 23 | * store the historyInstance 24 | */ 25 | this.childRoutes = []; 26 | this.historyInstance = hashHistory; 27 | 28 | /* 29 | * Set root-application routes 30 | * and assign the childRoutes 31 | */ 32 | this.routes = { 33 | childRoutes : [{ 34 | path : '/', 35 | component : appContainer, 36 | indexRoute : { 37 | component : indexComponent 38 | }, 39 | childRoutes : this.childRoutes 40 | }] 41 | }; 42 | } 43 | 44 | /** 45 | * @returns ReactRouter 46 | */ 47 | public getHistory = (): ReactRouter.History => 48 | { 49 | return this.historyInstance; 50 | } 51 | 52 | /** 53 | * @param {Redux.Store} store 54 | * @returns IRouting 55 | */ 56 | public syncHistoryWithStore = (store: Redux.Store): IRouting => 57 | { 58 | this.syncdHistory = syncHistoryWithStore(this.historyInstance, store); 59 | 60 | return this; 61 | }; 62 | 63 | /** 64 | * @returns ReactRouterRedux 65 | */ 66 | public getSyncdHistory = (): ReactRouterRedux.ReactRouterReduxHistory => 67 | { 68 | return this.syncdHistory; 69 | } 70 | 71 | /** 72 | * @returns ReactRouter 73 | */ 74 | public getRoutes = (): ReactRouter.PlainRoute => 75 | { 76 | return this.routes; 77 | } 78 | 79 | /** 80 | * @param {ReactRouter.PlainRoute} routeConfig 81 | */ 82 | public addRouteConfig = (routeConfig: ReactRouter.PlainRoute) => 83 | { 84 | this.childRoutes.push(routeConfig); 85 | 86 | return this; 87 | } 88 | 89 | /** 90 | * @param {ReactRouter.PlainRoute[]} ...routingConfigurations 91 | */ 92 | public addRouteConfigs = (...routingConfigurations: ReactRouter.PlainRoute[]) => 93 | { 94 | routingConfigurations.forEach(c => this.addRouteConfig(c)); 95 | 96 | return this; 97 | } 98 | }; 99 | 100 | export default Routing; -------------------------------------------------------------------------------- /src/js/Core/StoreCreator.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { 4 | compose, 5 | createStore, 6 | applyMiddleware, 7 | combineReducers 8 | } from 'redux'; 9 | import { 10 | routerReducer, 11 | routerMiddleware 12 | } from 'react-router-redux'; 13 | import { 14 | persistStore, 15 | autoRehydrate 16 | } from 'redux-persist'; 17 | import thunk from 'redux-thunk'; 18 | 19 | import Reducers from 'Reducers/Index'; 20 | import * as objectAssign from 'object-assign'; 21 | 22 | class StoreCreator implements IStoreCreator 23 | { 24 | private store: Redux.Store; 25 | 26 | /** 27 | * @param {ReactRouter.History} historyInstance 28 | */ 29 | constructor(historyInstance: ReactRouter.History, 30 | rehydrateCallback: () => void) 31 | { 32 | /* 33 | * Setup the middlewares and reducers 34 | */ 35 | const router = routerMiddleware(historyInstance); 36 | const reducers = combineReducers(objectAssign({}, Reducers, { 37 | routing : routerReducer 38 | })); 39 | 40 | /* 41 | * Further middleware and enhancer prep 42 | */ 43 | const middlewares = [thunk, router]; 44 | const enhancer = compose(applyMiddleware(...middlewares), autoRehydrate()); 45 | 46 | /* 47 | * Create our store with the reducers and enhancers. 48 | * Persist this state in localStorage, minus routing! 49 | */ 50 | this.store = createStore(reducers, undefined, enhancer) as Redux.Store; 51 | persistStore(this.store, { 52 | blacklist : [ 53 | 'routing', 54 | 'authentication', 55 | 'setup', 56 | 'appAlerts', 57 | 'notificationFilters' 58 | ] 59 | }, rehydrateCallback); 60 | } 61 | 62 | /** 63 | * @returns Redux 64 | */ 65 | public getStore(): Redux.Store 66 | { 67 | return this.store; 68 | }; 69 | 70 | }; 71 | 72 | export default StoreCreator; -------------------------------------------------------------------------------- /src/js/Electron/Menus/Accounts.ts: -------------------------------------------------------------------------------- 1 | import { replace } from 'react-router-redux'; 2 | 3 | import { 4 | getNewRemoteElectronMenu, 5 | getNewRemoteElectronMenuItem 6 | } from 'Helpers/System/Electron'; 7 | import { dispatch } from 'Helpers/State/Store'; 8 | 9 | import { updateAccount } from 'Actions/Accounts'; 10 | 11 | export default function createMenu(accountId: number): Electron.Menu 12 | { 13 | let menu = getNewRemoteElectronMenu(); 14 | 15 | /* 16 | * 17 | */ 18 | menu.append(getNewRemoteElectronMenuItem({ 19 | label : 'Update Details', 20 | click : () => dispatch(updateAccount(accountId.toString(), true)) 21 | })); 22 | 23 | /* 24 | * Separate! 25 | */ 26 | menu.append(getNewRemoteElectronMenuItem({ 27 | type : 'separator' 28 | })); 29 | 30 | /* 31 | * Settings 32 | */ 33 | menu.append(getNewRemoteElectronMenuItem({ 34 | label : 'Manage', 35 | click : () => dispatch(replace('/settings/accounts/' + accountId)) 36 | })); 37 | 38 | return menu; 39 | }; -------------------------------------------------------------------------------- /src/js/Electron/Menus/Notification.ts: -------------------------------------------------------------------------------- 1 | import { 2 | openExternalUrl, 3 | copyStringToClipboard, 4 | getNewRemoteElectronMenu, 5 | getNewRemoteElectronMenuItem 6 | } from 'Helpers/System/Electron'; 7 | import { 8 | getNotificationWebUrl, 9 | getNotificationSubjectPrettyName 10 | } from 'Helpers/Services/GitHub'; 11 | import { dispatch } from 'Helpers/State/Store'; 12 | 13 | import { handleMarkNotificationAsRead } from 'Actions/Notifications'; 14 | 15 | /** 16 | * @param {string} accountId 17 | * @param {IGitHubNotification} notification 18 | */ 19 | export default function createMenu(accountId: string, notification: IGitHubNotification) 20 | { 21 | let menu = getNewRemoteElectronMenu(); 22 | let subjectPrettyName = getNotificationSubjectPrettyName(notification.subject.type); 23 | 24 | /* 25 | * Open In Browser 26 | */ 27 | menu.append(getNewRemoteElectronMenuItem({ 28 | label : 'Open in Browser', 29 | click : () => openExternalUrl(getNotificationWebUrl(notification)) 30 | })); 31 | 32 | /* 33 | * Copy X Link 34 | */ 35 | menu.append(getNewRemoteElectronMenuItem({ 36 | label : 'Copy ' + subjectPrettyName + ' Link', 37 | click : () => copyStringToClipboard(getNotificationWebUrl(notification)) 38 | })); 39 | 40 | /* 41 | * Copy X Title 42 | */ 43 | menu.append(getNewRemoteElectronMenuItem({ 44 | label : 'Copy ' + subjectPrettyName + ' Title', 45 | click : () => copyStringToClipboard(notification.subject.title) 46 | })); 47 | 48 | /* 49 | * Separate! 50 | */ 51 | menu.append(getNewRemoteElectronMenuItem({ 52 | type : 'separator' 53 | })); 54 | 55 | /* 56 | * Mark Notification as Read 57 | */ 58 | menu.append(getNewRemoteElectronMenuItem({ 59 | label : 'Mark as Read', 60 | click : () => dispatch(handleMarkNotificationAsRead(accountId, notification.id.toString())) 61 | })) 62 | 63 | return menu; 64 | }; -------------------------------------------------------------------------------- /src/js/Electron/Menus/Repository.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getNewRemoteElectronMenu, 3 | getNewRemoteElectronMenuItem 4 | } from 'Helpers/System/Electron'; 5 | import { dispatch } from 'Helpers/State/Store'; 6 | 7 | import { setupRepositoryMuteFilter } from 'Actions/UIActions/RepositoryMuteFilters'; 8 | 9 | export default function createMenu(accountId: number, repository: IGitHubRepository): Electron.Menu 10 | { 11 | let menu = getNewRemoteElectronMenu(); 12 | 13 | menu.append(getNewRemoteElectronMenuItem({ 14 | label : 'Edit Mute Filter', 15 | click : () => dispatch(setupRepositoryMuteFilter( 16 | accountId, 17 | repository, 18 | 'settings/accounts/' 19 | + accountId 20 | + '/repo-mute-filter/' 21 | + repository.id 22 | )) 23 | })); 24 | 25 | return menu; 26 | }; -------------------------------------------------------------------------------- /src/js/Electron/Tasks/App.ts: -------------------------------------------------------------------------------- 1 | import { dispatch } from 'Helpers/State/Store'; 2 | import { pushAppAlert } from 'Actions/AppAlerts'; 3 | import { getElectron } from 'Helpers/System/Electron'; 4 | import { updateActions } from 'Constants/System/Electron'; 5 | import { createSuccessAppAlert } from 'Helpers/Models/AppAlert'; 6 | 7 | /** 8 | */ 9 | export function autoUpdateQuitAndInstall() 10 | { 11 | getElectron() 12 | .ipcRenderer 13 | .send(updateActions.AU_QUIT_INSTALL); 14 | }; 15 | 16 | export function registerUpdateAvailableHandler() 17 | { 18 | getElectron() 19 | .ipcRenderer 20 | .on(updateActions.AU_UPDATE_AVAILABLE, () => dispatch(pushAppAlert(createSuccessAppAlert( 21 | 'Update available. Click the refresh icon to quit and install', 22 | true, { 23 | stickyActionIcon : 'sticky', 24 | stickyActionName : updateActions.AU_QUIT_INSTALL, 25 | stickyActionParams : {} 26 | } 27 | )))); 28 | }; -------------------------------------------------------------------------------- /src/js/Electron/Tasks/Notification.ts: -------------------------------------------------------------------------------- 1 | import { dispatch } from 'Helpers/State/Store'; 2 | import { getElectron } from 'Helpers/System/Electron'; 3 | 4 | import { markNotificationAsRead } from 'Actions/Notifications'; 5 | 6 | const MarkNotificationAsReadTask = 'MarkNotificationRead'; 7 | const MarkNotificationAsReadSuccessTask = 'MarkNotificationReadSuccess'; 8 | 9 | /** 10 | * @param {string} token 11 | * @param {string} accountId 12 | * @param {string[]} notificationIds 13 | */ 14 | export function markMultipleNotificationsAsRead(token: string, 15 | accountId: number, 16 | notificationIds: string[]) 17 | { 18 | let ipc = getElectron().ipcRenderer; 19 | 20 | notificationIds.forEach(id => ipc.send(MarkNotificationAsReadTask, [{ 21 | token : token, 22 | accountId : accountId, 23 | notificationId : id, 24 | }])); 25 | }; 26 | 27 | /** 28 | */ 29 | export function registerMarkNotificationAsReadSuccess() 30 | { 31 | getElectron() 32 | .ipcRenderer 33 | .on(MarkNotificationAsReadSuccessTask, (e, args: { token: string; 34 | accountId: string; 35 | notificationId: string; }) => 36 | { 37 | dispatch(markNotificationAsRead(args.accountId, args.notificationId)); 38 | }); 39 | }; -------------------------------------------------------------------------------- /src/js/Electron/Tasks/Settings.ts: -------------------------------------------------------------------------------- 1 | import { replace } from 'react-router-redux'; 2 | 3 | import { dispatch } from 'Helpers/State/Store'; 4 | import { getElectron } from 'Helpers/System/Electron'; 5 | 6 | const PreferencesTask = 'Preferences'; 7 | 8 | export function registerPreferencesMenuControl() 9 | { 10 | getElectron() 11 | .ipcRenderer 12 | .on(PreferencesTask, () => dispatch(replace('/settings'))); 13 | }; -------------------------------------------------------------------------------- /src/js/Filter/Filter.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import * as clone from 'lodash/clone'; 5 | 6 | class Filter implements IFilter 7 | { 8 | private filterFunctions: IFilterFunction[] = []; 9 | 10 | private originalDataSet: T[] = []; 11 | 12 | private filteredDataSet: T[] = []; 13 | 14 | private ruleSet: any = {}; 15 | 16 | constructor(dataSet: T[], ruleSet: any) 17 | { 18 | this.originalDataSet = dataSet; 19 | this.ruleSet = ruleSet; 20 | } 21 | 22 | public addFilterFunction = (filterFunc: IFilterFunction): IFilter => 23 | { 24 | this.filterFunctions.push(filterFunc); 25 | 26 | return this; 27 | } 28 | 29 | public addFilterFunctions = (...filterFunctions: IFilterFunction[]): IFilter => 30 | { 31 | filterFunctions.forEach(f => this.filterFunctions.push(f)); 32 | 33 | return this; 34 | } 35 | 36 | public filter = (): T[] => 37 | { 38 | this.filteredDataSet = clone(this.originalDataSet); 39 | for (var i = 0; i < this.filterFunctions.length; i++) { 40 | let func = this.filterFunctions[i]; 41 | 42 | this.filteredDataSet = func(this.filteredDataSet, this.ruleSet); 43 | } 44 | 45 | return this.filteredDataSet; 46 | } 47 | 48 | }; 49 | 50 | export default Filter; -------------------------------------------------------------------------------- /src/js/Filter/FilterFunctions/GitHubNotifications/Index.ts: -------------------------------------------------------------------------------- 1 | import Read from './Read'; 2 | import Reason from './Reason'; 3 | import Subject from './Subject'; 4 | import Repository from './Repository'; 5 | 6 | export { 7 | Read, 8 | Reason, 9 | Subject, 10 | Repository 11 | }; -------------------------------------------------------------------------------- /src/js/Filter/FilterFunctions/GitHubNotifications/Read.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {IGitHubNotification[]} input 4 | * @param {INotificationFilterSet} ruleSet 5 | * @returns IGitHubNotification 6 | */ 7 | export default function filter(input: IGitHubNotification[], ruleSet: INotificationFilterSet): IGitHubNotification[] 8 | { 9 | return input.filter(n => ruleSet.read != n.unread); 10 | }; -------------------------------------------------------------------------------- /src/js/Filter/FilterFunctions/GitHubNotifications/Reason.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {IGitHubNotification[]} input 4 | * @param {INotificationFilterSet} ruleSet 5 | * @returns IGitHubNotification 6 | */ 7 | export default function filter(input: IGitHubNotification[], ruleSet: INotificationFilterSet): IGitHubNotification[] 8 | { 9 | if (ruleSet.reasonType.length === 0) { 10 | return input; 11 | } 12 | 13 | return input.filter(n => ruleSet.reasonType.indexOf(n.reason) > -1); 14 | }; -------------------------------------------------------------------------------- /src/js/Filter/FilterFunctions/GitHubNotifications/Repository.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {IGitHubNotification[]} input 4 | * @param {INotificationFilterSet} ruleSet 5 | * @returns IGitHubNotification 6 | */ 7 | export default function filter(input: IGitHubNotification[], ruleSet: INotificationFilterSet): IGitHubNotification[] 8 | { 9 | if (ruleSet.repository.length === 0) { 10 | return input; 11 | } 12 | 13 | return input.filter(n => ruleSet.repository.indexOf(n.repository.id) > -1); 14 | }; -------------------------------------------------------------------------------- /src/js/Filter/FilterFunctions/GitHubNotifications/Subject.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {IGitHubNotification[]} input 4 | * @param {INotificationFilterSet} ruleSet 5 | * @returns IGitHubNotification 6 | */ 7 | export default function filter(input: IGitHubNotification[], ruleSet: INotificationFilterSet): IGitHubNotification[] 8 | { 9 | if (ruleSet.subjectType.length === 0) { 10 | return input; 11 | } 12 | 13 | return input.filter(n => ruleSet.subjectType.indexOf(n.subject.type) > -1); 14 | }; -------------------------------------------------------------------------------- /src/js/Filter/Interfaces/IFilter.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface IFilter 4 | { 5 | addFilterFunction(filterFunc: IFilterFunction): IFilter; 6 | 7 | addFilterFunctions(...filterFunctions: IFilterFunction[]): IFilter; 8 | 9 | filter(): T[]; 10 | }; -------------------------------------------------------------------------------- /src/js/Filter/Interfaces/IFilterFunction.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IFilterFunction 3 | { 4 | (input: T[], ruleSet: any): T[]; 5 | }; -------------------------------------------------------------------------------- /src/js/GitHub/Activity.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | class Activity implements IGitHubActivity 4 | { 5 | private requestFactory: IRequestFactory; 6 | 7 | constructor(requestFactory: IRequestFactory) 8 | { 9 | this.requestFactory = requestFactory; 10 | } 11 | 12 | public getNotifications = (token: string, 13 | page: number = 1, 14 | all: boolean = false, 15 | participating: boolean = false, 16 | since?: string, 17 | before?: string): Promise => 18 | { 19 | let query = { 20 | access_token : token, 21 | all : all, 22 | participating : participating, 23 | page : page 24 | }; 25 | 26 | if (typeof since === 'string') { 27 | query['since'] = since; 28 | } 29 | 30 | if (typeof before === 'string') { 31 | query['before'] = before; 32 | } 33 | 34 | return new Promise((resolve, reject) => 35 | { 36 | this.requestFactory 37 | .newRequest() 38 | .setUrl('/notifications') 39 | .setQuery(query) 40 | .execute() 41 | .then(response => response.json()) 42 | .then(res => resolve(res), 43 | err => reject(err)); 44 | }); 45 | } 46 | 47 | public markThreadAsRead = (token: string, threadId: string): Promise => 48 | { 49 | return new Promise((resolve, reject) => 50 | { 51 | this.requestFactory 52 | .newRequest() 53 | .setUrl('/notifications/threads/' + threadId) 54 | .setQuery({ 55 | access_token : token 56 | }) 57 | .setMethod('post') 58 | .execute() 59 | .then(response => 60 | { 61 | if (response.status === 205) { 62 | resolve(); 63 | return; 64 | } 65 | 66 | reject(); 67 | }); 68 | }); 69 | } 70 | }; 71 | 72 | export default Activity; -------------------------------------------------------------------------------- /src/js/GitHub/Authentication.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as QueryString from 'query-string'; 4 | 5 | class Authentication implements IGitHubAuthentication 6 | { 7 | private requestFactory: IRequestFactory; 8 | 9 | constructor(requestFactory: IRequestFactory) 10 | { 11 | this.requestFactory = requestFactory; 12 | } 13 | 14 | /** 15 | * @param {string} clientId 16 | * @param {string[]} scopes 17 | * @returns Promise 18 | */ 19 | public generateOAuthUrl = (clientId: string, scopes: string[]): Promise => 20 | { 21 | return new Promise(resolve => 22 | { 23 | let url = 'https://github.com/login/oauth/authorize?'; 24 | let qs = { 25 | client_id : clientId, 26 | scope : scopes.join(' ') 27 | }; 28 | 29 | resolve(url + QueryString.stringify(qs)); 30 | }); 31 | } 32 | 33 | /** 34 | * @param {string} clientId 35 | * @param {string} clientSecret 36 | * @param {string} code 37 | * @returns Promise 38 | */ 39 | public authenticateAccessToken = (clientId: string, clientSecret: string, code: string): Promise => 40 | { 41 | return new Promise((resolve, reject) => 42 | { 43 | let body = { 44 | client_id : clientId, 45 | client_secret : clientSecret, 46 | code 47 | }; 48 | 49 | // @todo: Host set :/ Extract? 50 | this.requestFactory 51 | .newRequest() 52 | .setHost('https://github.com') 53 | .setUrl('/login/oauth/access_token') 54 | .setMethod('post') 55 | .setBody(body) 56 | .sendAsJson() 57 | .execute() 58 | .then(r => r.json()) 59 | .then(res => 60 | { 61 | // @todo: IMPROVE THIS ERROR HANDLING. 62 | if (typeof res.access_token === 'undefined') { 63 | reject(null); 64 | } 65 | 66 | resolve(res.access_token); 67 | }, err => reject(null)); 68 | }); 69 | } 70 | }; 71 | 72 | export default Authentication; -------------------------------------------------------------------------------- /src/js/GitHub/GitHub.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import Users from './Users'; 5 | import Activity from './Activity'; 6 | import Authentication from './Authentication'; 7 | 8 | class GitHub implements IGitHub 9 | { 10 | private requestFactory: IRequestFactory; 11 | 12 | public activity: IGitHubActivity; 13 | 14 | public users: IGitHubUsers; 15 | 16 | public authentication: IGitHubAuthentication; 17 | 18 | constructor(requestFactory: IRequestFactory) 19 | { 20 | this.requestFactory = requestFactory; 21 | 22 | this.activity = new Activity(this.requestFactory); 23 | this.users = new Users(this.requestFactory); 24 | this.authentication = new Authentication(this.requestFactory); 25 | } 26 | }; 27 | 28 | export default GitHub; 29 | -------------------------------------------------------------------------------- /src/js/GitHub/Interfaces/IGitHub.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | interface IGitHub 6 | { 7 | activity: IGitHubActivity; 8 | 9 | users: IGitHubUsers; 10 | 11 | authentication: IGitHubAuthentication; 12 | }; -------------------------------------------------------------------------------- /src/js/GitHub/Interfaces/IGitHubActivity.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IGitHubActivity 3 | { 4 | getNotifications(token: string, 5 | page?: number, 6 | all?: boolean, 7 | participating?: boolean, 8 | since?: string, 9 | before?: string): Promise; 10 | 11 | markThreadAsRead(token: string, 12 | threadId: string): Promise; 13 | }; -------------------------------------------------------------------------------- /src/js/GitHub/Interfaces/IGitHubAuthentication.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IGitHubAuthentication 3 | { 4 | generateOAuthUrl(clientId: string, scopes: string[]): Promise; 5 | 6 | authenticateAccessToken(clientId: string, clientSecret: string, code: string): Promise; 7 | }; -------------------------------------------------------------------------------- /src/js/GitHub/Interfaces/IGitHubUsers.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IGitHubUsers 3 | { 4 | getAuthenticatedUser(token: string): Promise; 5 | }; -------------------------------------------------------------------------------- /src/js/GitHub/Users.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | class Users implements IGitHubUsers 4 | { 5 | private requestFactory: IRequestFactory; 6 | 7 | constructor(requestFactory: IRequestFactory) 8 | { 9 | this.requestFactory = requestFactory; 10 | } 11 | 12 | public getAuthenticatedUser = (token: string): Promise => 13 | { 14 | return new Promise((resolve, reject) => 15 | { 16 | this.requestFactory 17 | .newRequest() 18 | .setUrl('/user') 19 | .setQuery({ 20 | access_token : token 21 | }) 22 | .execute() 23 | .then(response => response.json()) 24 | .then(res => resolve(res), 25 | err => reject(err)); 26 | }); 27 | } 28 | 29 | }; 30 | 31 | export default Users; -------------------------------------------------------------------------------- /src/js/HawkEye.ts: -------------------------------------------------------------------------------- 1 | import InstanceCache from 'Core/InstanceCache'; 2 | import { renderApplication } from 'Core/Renderer'; 3 | 4 | import App from 'View/App'; 5 | import AppIndex from 'View/Index'; 6 | 7 | import Routing from 'Core/Routing'; 8 | import StoreCreator from 'Core/StoreCreator'; 9 | import RequestFactory from 'Core/RequestFactory'; 10 | 11 | import GitHub from 'GitHub/GitHub'; 12 | import Scheduler from 'Scheduler/Scheduler'; 13 | 14 | import { gitHubApiUrl } from 'Constants/Services/GitHub'; 15 | import { appSetupFlow } from 'Actions/HawkEye'; 16 | 17 | import GitHubAccountsService from 'Services/GitHubAccountsService'; 18 | import GitHubAuthenticationService from 'Services/GitHubAuthenticationService'; 19 | import GitHubNotificationsService from 'Services/GitHubNotificationsService'; 20 | 21 | import Routes from 'Routes'; 22 | 23 | class HawkEye 24 | { 25 | private routing: IRouting; 26 | 27 | private storeCreator: IStoreCreator; 28 | 29 | constructor() 30 | { 31 | /* 32 | * Setup Routing and StoreCreator for our application 33 | */ 34 | this.routing = new Routing(App, AppIndex); 35 | this.storeCreator = new StoreCreator(this.routing.getHistory(), 36 | this.onRehydrated.bind(this)); 37 | 38 | /* 39 | * Sync history with the Store and add 40 | * the routing configuration 41 | */ 42 | this.routing 43 | .syncHistoryWithStore(this.storeCreator.getStore()) 44 | .addRouteConfigs(...Routes); 45 | 46 | /* 47 | * Bind our Routing and StoreCreator to the cache 48 | */ 49 | InstanceCache 50 | .addInstance('IRouting', this.routing) 51 | .addInstance>('IStoreCreator', this.storeCreator) 52 | .addInstance('IScheduler', new Scheduler); 53 | 54 | // @todo: Migrate the lot. ServiceBinder 55 | const gitHubService = new GitHub(new RequestFactory(gitHubApiUrl)); 56 | 57 | InstanceCache 58 | .addInstance('IGitHub', gitHubService) 59 | .addInstance('IGitHubAccountsService', 60 | new GitHubAccountsService(gitHubService)) 61 | .addInstance('IGitHubAuthenticationService', 62 | new GitHubAuthenticationService(gitHubService)) 63 | .addInstance('IGitHubNotificationsService', 64 | new GitHubNotificationsService(gitHubService)); 65 | 66 | /* 67 | * Render our application in the container, 68 | * along with our storeCreator and routing instances. 69 | */ 70 | renderApplication( 71 | document.getElementById('root'), 72 | this.storeCreator, 73 | this.routing 74 | ); 75 | } 76 | 77 | public onRehydrated = () => 78 | { 79 | this.storeCreator.getStore().dispatch(appSetupFlow()); 80 | } 81 | }; 82 | 83 | export default HawkEye; -------------------------------------------------------------------------------- /src/js/Helpers/Lang/Array.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {T[]} array 4 | * @returns T 5 | */ 6 | export function getLast(array: T[]): T 7 | { 8 | if (array.length === 0) { 9 | return null; 10 | } 11 | 12 | return array[array.length - 1]; 13 | }; 14 | 15 | /** 16 | * @param {any[]} array 17 | */ 18 | export function hasItems(array: any[]) 19 | { 20 | return array.length > 0; 21 | } 22 | 23 | /** 24 | * @param {any[]} array 25 | * @param {number} min 26 | */ 27 | export function hasMinItems(array: any[], min: number) 28 | { 29 | return array.length > min; 30 | }; 31 | 32 | /** 33 | * @param {number} index 34 | */ 35 | export function isFirstItem(index: number) 36 | { 37 | return index === 0; 38 | }; 39 | 40 | /** 41 | * @param {any[]} array 42 | * @param {number} index 43 | */ 44 | export function isLastItem(array: any[], index: number) 45 | { 46 | return array.length === index + 1; 47 | }; 48 | 49 | /** 50 | * @param {any[]} array 51 | * @param {(key:any)=>any} getValue 52 | * @returns any 53 | */ 54 | export function toObject(array: any[], 55 | getKey: (value: any, key: any) => any, 56 | getValue: (value: any, key: any) => any): any 57 | { 58 | let object = {}; 59 | 60 | array.forEach((v, i) => object[getKey(v, i)] = getValue(v, i)); 61 | 62 | return object; 63 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Lang/Audio.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @returns boolean 4 | */ 5 | export function hasAudioCapability(): boolean 6 | { 7 | return typeof Audio !== 'undefined'; 8 | } 9 | 10 | /** 11 | * @param {string} sound 12 | */ 13 | export function playSound(sound: string) 14 | { 15 | if (!hasAudioCapability()) { 16 | return; 17 | } 18 | 19 | new Audio(sound).play(); 20 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Lang/Date.ts: -------------------------------------------------------------------------------- 1 | import * as Moment from 'moment'; 2 | 3 | import { cronPeriods } from 'Constants/Lang/Date'; 4 | 5 | /** 6 | * @param {string} name 7 | * @returns string 8 | */ 9 | export function getCronPeriodByName(name: string): string 10 | { 11 | return cronPeriods[name] || null; 12 | }; 13 | 14 | /** 15 | * @param {} date? 16 | * @param {} format='YYYY-MM-DD' 17 | * @returns string 18 | */ 19 | export function formatDate(date?, format = 'YYYY-MM-DD'): string 20 | { 21 | return Moment(date).format(format); 22 | }; 23 | 24 | /** 25 | * @param {string|moment.Moment} date 26 | * @returns moment 27 | */ 28 | export function toUtc(date: string | moment.Moment): moment.Moment 29 | { 30 | return Moment(date).utc(); 31 | }; 32 | 33 | /** 34 | * @param {} date? 35 | * @returns string 36 | */ 37 | export function formatDateAsUTC(date?): string 38 | { 39 | return formatDate(date, 'YYYY-MM-DDTHH:mm:ss') + 'Z'; 40 | }; 41 | 42 | /** 43 | * @param {} date? 44 | * @returns moment 45 | */ 46 | export function convertUtcToLocal(date?): moment.Moment 47 | { 48 | return Moment(date).utc().local(); 49 | }; 50 | 51 | export function relativeTime(date: string | moment.Moment): string 52 | { 53 | return Moment(date).fromNow(false); 54 | }; 55 | 56 | /** 57 | * @param {string|moment.Moment} date 58 | * @param {string} unit 59 | * @param {number} amount 60 | * @returns moment 61 | */ 62 | export function addTime(date: string | moment.Moment, unit: string, amount: number): moment.Moment 63 | { 64 | return Moment(date).add(unit, amount); 65 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Lang/Sort.ts: -------------------------------------------------------------------------------- 1 | 2 | export const sortingMethods = { 3 | dateAsc : sortDateAsc, 4 | dateDesc : sortDateDesc 5 | }; 6 | 7 | /** 8 | * @param {string} field? 9 | * @returns any 10 | */ 11 | export function sortDateAsc(field?: string): any 12 | { 13 | return (a, b) => 14 | { 15 | let dateOne = new Date(typeof field !== 'undefined' 16 | ? a[field] 17 | : a); 18 | let dateTwo = new Date(typeof field !== 'undefined' 19 | ? b[field] 20 | : b); 21 | 22 | if (dateOne > dateTwo) return 1; 23 | if (dateOne < dateTwo) return -1; 24 | }; 25 | }; 26 | 27 | export function sortDateDesc(field?: string): any 28 | { 29 | return (a, b) => 30 | { 31 | let dateOne = new Date(typeof field !== 'undefined' 32 | ? a[field] 33 | : a); 34 | let dateTwo = new Date(typeof field !== 'undefined' 35 | ? b[field] 36 | : b); 37 | 38 | if (dateOne > dateTwo) return -1; 39 | if (dateOne < dateTwo) return 1; 40 | }; 41 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Lang/String.ts: -------------------------------------------------------------------------------- 1 | 2 | const possibleChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 3 | 4 | /** 5 | * @param {number=10} length 6 | * @returns string 7 | */ 8 | export function randomString(length: number = 10): string 9 | { 10 | let text: string = ''; 11 | for(var i = 0; i < length; i++) { 12 | text += possibleChars.charAt( 13 | Math.floor(Math.random() * possibleChars.length) 14 | ); 15 | } 16 | 17 | return text; 18 | } 19 | 20 | /** 21 | */ 22 | export function generateId() 23 | { 24 | return randomString() + (new Date().getTime()); 25 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Lang/Timeout.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {number} duration 4 | * @returns Promise 5 | */ 6 | export function wait(duration: number): Promise 7 | { 8 | return new Promise(resolve => setTimeout(resolve, duration)); 9 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Models/Accounts.ts: -------------------------------------------------------------------------------- 1 | import { getState } from 'Helpers/State/Store'; 2 | 3 | /** 4 | * @returns string 5 | */ 6 | export function getAccountIds(): string[] 7 | { 8 | return Object.keys(getState().accounts); 9 | }; 10 | 11 | /** 12 | * @param {string} accountId 13 | * @returns string 14 | */ 15 | export function getAccountToken(accountId: string): string 16 | { 17 | let account = getAccount(accountId); 18 | if (account === null) { 19 | return null; 20 | } 21 | 22 | return account.token; 23 | }; 24 | 25 | /** 26 | * @param {string} accountId 27 | * @returns IStateAccountsAccount 28 | */ 29 | export function getAccount(accountId: string): IStateAccountsAccount 30 | { 31 | let accounts = getState().accounts; 32 | if (typeof accounts[accountId] === 'undefined') { 33 | return null; 34 | } 35 | 36 | return accounts[accountId]; 37 | }; 38 | 39 | /** 40 | * @param {string} accountId 41 | * @returns boolean 42 | */ 43 | export function accountAlreadyAdded(accountId: string): boolean 44 | { 45 | return getAccount(accountId) != null; 46 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Models/App.ts: -------------------------------------------------------------------------------- 1 | import { getState } from 'Helpers/State/Store'; 2 | 3 | /** 4 | * @returns boolean 5 | */ 6 | export function isPolling(): boolean 7 | { 8 | return getState().app.isPolling; 9 | }; 10 | 11 | /** 12 | * @returns string 13 | */ 14 | export function getLastPoll(): string 15 | { 16 | return getState().app.lastPoll; 17 | }; 18 | 19 | /** 20 | * @returns number 21 | */ 22 | export function getCurrentAccountId(): number 23 | { 24 | return getState().app.currentAccountId; 25 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Models/GitHubNotificationFilterSet.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {IGitHubNotification[]} notifications 4 | * @returns IGitHubNotificationFilterSet 5 | */ 6 | export function createGitHubNotificationFilterSet(notifications: IGitHubNotification[], 7 | filterRules: INotificationFilterSet): IGitHubNotificationFilterSet 8 | { 9 | let filterOpts = { 10 | read : 0, 11 | subjectTypes : {}, 12 | reasonTypes : {}, 13 | repositories : {} 14 | }; 15 | 16 | /* 17 | * Go through each and create the numbers 18 | * @todo: change this, perf. 19 | */ 20 | notifications 21 | .forEach(notification => 22 | { 23 | if (!notification.unread) { 24 | filterOpts.read++; 25 | } 26 | 27 | if (filterRules.read 28 | && notification.unread) { 29 | return; 30 | } 31 | 32 | if (!filterRules.read 33 | && !notification.unread) { 34 | return; 35 | } 36 | 37 | /* 38 | * Subject Types 39 | */ 40 | if (typeof filterOpts.subjectTypes[notification.subject.type] === 'undefined') { 41 | filterOpts.subjectTypes[notification.subject.type] = 0; 42 | } 43 | 44 | filterOpts.subjectTypes[notification.subject.type]++; 45 | 46 | /* 47 | * Reason Types 48 | */ 49 | if (typeof filterOpts.reasonTypes[notification.reason] === 'undefined') { 50 | filterOpts.reasonTypes[notification.reason] = 0; 51 | } 52 | 53 | filterOpts.reasonTypes[notification.reason]++; 54 | 55 | /* 56 | * Repositories 57 | */ 58 | if (typeof filterOpts.repositories[notification.repository.id] === 'undefined') { 59 | filterOpts.repositories[notification.repository.id] = { 60 | repository : notification.repository, 61 | count : 0 62 | } 63 | } 64 | 65 | filterOpts.repositories[notification.repository.id].count++; 66 | }); 67 | 68 | let notificationFilters: IGitHubNotificationFilterSet = { 69 | read : filterOpts.read, 70 | subjectTypes : Object.keys(filterOpts.subjectTypes) 71 | .map(type => ({ 72 | name : type, 73 | count : filterOpts.subjectTypes[type] 74 | })), 75 | reasonTypes : Object.keys(filterOpts.reasonTypes) 76 | .map(type => ({ 77 | name : type, 78 | count : filterOpts.reasonTypes[type] 79 | })), 80 | repositories : Object.keys(filterOpts.repositories) 81 | .map(repoId => ({ 82 | repository : filterOpts.repositories[repoId].repository, 83 | count : filterOpts.repositories[repoId].count 84 | })) 85 | }; 86 | 87 | return notificationFilters; 88 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Models/GitHubRepository.ts: -------------------------------------------------------------------------------- 1 | import { makeGitHubUser } from './GitHubUser'; 2 | 3 | /** 4 | * @param {any} repo 5 | * @returns IGitHubRepository 6 | */ 7 | export function makeGitHubRepository(repo: any): IGitHubRepository 8 | { 9 | if (typeof repo.id === 'undefined') { 10 | return null; 11 | } 12 | 13 | return { 14 | id : repo.id, 15 | name : repo.name, 16 | fullName : repo.full_name, 17 | private : repo.private, 18 | htmlUrl : repo.html_url, 19 | owner : makeGitHubUser(repo.owner) 20 | }; 21 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Models/GitHubUser.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {any} user 4 | * @returns IGitHubUser 5 | */ 6 | export function makeGitHubUser(user: any): IGitHubUser 7 | { 8 | if (typeof user.id === 'undefined') { 9 | return null; 10 | } 11 | 12 | let newUser: IGitHubUser = { 13 | id : user.id, 14 | avatarUrl : user.avatar_url, 15 | name : user.name, 16 | email : user.email, 17 | createdAt : user.created_at, 18 | username : user.login 19 | }; 20 | 21 | return newUser; 22 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Models/RepositoryMuteFilters.ts: -------------------------------------------------------------------------------- 1 | import { getState } from 'Helpers/State/Store'; 2 | 3 | /** 4 | * @param {number} accountId 5 | * @returns IStateRepositoryMuteFiltersAccount 6 | */ 7 | export function getAccountRepositoryMuteFilters(accountId: string): IStateRepositoryMuteFiltersAccount 8 | { 9 | return getState().repositoryMuteFilters[accountId] || {}; 10 | }; -------------------------------------------------------------------------------- /src/js/Helpers/Models/Settings.ts: -------------------------------------------------------------------------------- 1 | import { getState } from 'Helpers/State/Store'; 2 | 3 | /** 4 | * @returns string 5 | */ 6 | export function getCurrentPollPeriod(): string 7 | { 8 | return getState().settings.pollPeriod; 9 | }; 10 | 11 | /** 12 | * @returns boolean 13 | */ 14 | export function alertSuccessSoundIsEnabled(): boolean 15 | { 16 | return getState().settings.soundSettings.alertSuccessEnabled; 17 | }; 18 | 19 | /** 20 | * @returns boolean 21 | */ 22 | export function alertErrorSoundIsEnabled(): boolean 23 | { 24 | return getState().settings.soundSettings.alertErrorEnabled; 25 | }; 26 | 27 | /** 28 | * @returns boolean 29 | */ 30 | export function newItemsSoundIsEnabled(): boolean 31 | { 32 | return getState().settings.soundSettings.newItemsEnabled; 33 | }; 34 | 35 | /** 36 | * @returns string 37 | */ 38 | export function getNotificationDoubleClickAction(): string 39 | { 40 | return getState().settings.notifications.doubleClickAction; 41 | }; 42 | 43 | /** 44 | * @returns boolean 45 | */ 46 | export function shouldConfirmBeforeMarkingNotificationsAsRead(): boolean 47 | { 48 | return getState().settings.notifications.confirmBeforeMarkingMultipleAsRead; 49 | }; -------------------------------------------------------------------------------- /src/js/Helpers/State/Reducify.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {any={}} defaultState 4 | * @param {any} reducingMethods 5 | */ 6 | const reducify = (defaultState: any = {}, reducingMethods: any) => 7 | { 8 | return (state = defaultState, action) => 9 | { 10 | return typeof reducingMethods[action.type] === 'function' 11 | ? reducingMethods[action.type](state, action) 12 | : state; 13 | }; 14 | }; 15 | 16 | export default reducify; -------------------------------------------------------------------------------- /src/js/Helpers/State/Store.ts: -------------------------------------------------------------------------------- 1 | import InstanceCache from 'Core/InstanceCache'; 2 | 3 | /** 4 | * @returns Redux 5 | */ 6 | export function getStore(): Redux.Store 7 | { 8 | return InstanceCache.getInstance>('IStoreCreator') 9 | .getStore(); 10 | }; 11 | 12 | /** 13 | * @param {any} action 14 | */ 15 | export function dispatch(action: any) 16 | { 17 | return getStore().dispatch(action); 18 | }; 19 | 20 | /** 21 | * @returns T 22 | */ 23 | export function getState(): T 24 | { 25 | return getStore().getState() as T; 26 | }; -------------------------------------------------------------------------------- /src/js/Helpers/System/Electron.ts: -------------------------------------------------------------------------------- 1 | const windowRequire = window['require'] as NodeRequire; 2 | 3 | /** 4 | * @returns Electron 5 | */ 6 | export function getElectron(): Electron.ElectronMainAndRenderer 7 | { 8 | return windowRequire('electron') as Electron.ElectronMainAndRenderer; 9 | }; 10 | 11 | /** 12 | * @returns Electron 13 | */ 14 | export function getNewRemoteElectronMenu(): Electron.Menu 15 | { 16 | let electron = getElectron(); 17 | 18 | return new electron.remote.Menu(); 19 | }; 20 | 21 | /** 22 | * @param {Electron.MenuItemOptions} opts 23 | * @returns Electron 24 | */ 25 | export function getNewRemoteElectronMenuItem(opts: Electron.MenuItemOptions): Electron.MenuItem 26 | { 27 | let electron = getElectron(); 28 | 29 | return new electron.remote.MenuItem(opts); 30 | }; 31 | 32 | /** 33 | * @param {string} string 34 | */ 35 | export function copyStringToClipboard(string: string) 36 | { 37 | getElectron().clipboard.writeText(string); 38 | }; 39 | 40 | /** 41 | * @param {string} url 42 | * @param {boolean=true} activate 43 | */ 44 | export function openExternalUrl(url: string, activate: boolean = true) 45 | { 46 | getElectron().shell.openExternal(url, { 47 | activate : activate 48 | }); 49 | }; 50 | 51 | /** 52 | * @returns string 53 | */ 54 | export function getPlatform(): string 55 | { 56 | return getElectron().remote.getGlobal('process').platform; 57 | }; 58 | 59 | /** 60 | * @returns boolean 61 | */ 62 | export function isMac(): boolean 63 | { 64 | return getPlatform() === 'darwin'; 65 | }; 66 | 67 | /** 68 | * @returns Electron 69 | */ 70 | export function getCurrentWindow(): Electron.BrowserWindow 71 | { 72 | return getElectron().remote.getCurrentWindow(); 73 | }; -------------------------------------------------------------------------------- /src/js/Helpers/System/Environment.ts: -------------------------------------------------------------------------------- 1 | const developmentEnv = 'development'; 2 | const productionEnv = 'production'; 3 | 4 | /** 5 | * @returns boolean 6 | */ 7 | export function isDevelopment(): boolean 8 | { 9 | return isEnv(developmentEnv); 10 | } 11 | 12 | /** 13 | * @returns boolean 14 | */ 15 | export function isProduction(): boolean 16 | { 17 | return isEnv(productionEnv); 18 | }; 19 | 20 | /** 21 | * @param {string} env 22 | * @returns boolean 23 | */ 24 | export function isEnv(env: string): boolean 25 | { 26 | return process.env.NODE_ENV === env; 27 | }; -------------------------------------------------------------------------------- /src/js/Helpers/System/Scheduler.ts: -------------------------------------------------------------------------------- 1 | import InstaceCache from 'Core/InstanceCache'; 2 | 3 | import { pollSinceNotifications } from 'Actions/Notifications'; 4 | 5 | import { 6 | isPolling, 7 | getLastPoll 8 | } from 'Helpers/Models/App'; 9 | import { 10 | getAccountIds, 11 | getAccountToken 12 | } from 'Helpers/Models/Accounts'; 13 | import { dispatch } from 'Helpers/State/Store'; 14 | import { getCronPeriodByName } from 'Helpers/Lang/Date'; 15 | 16 | /** 17 | * @param {string} pollPeriod 18 | */ 19 | export function configurePollingScheduler(pollPeriod: string) 20 | { 21 | let scheduler = InstaceCache.getInstance('IScheduler'); 22 | 23 | /* 24 | * Clear all the current jobs. We'll reconfigure them below. 25 | */ 26 | scheduler.clearAllJobs(); 27 | 28 | /* 29 | * Setup the job for polling 30 | */ 31 | let name = scheduler.scheduleJob(getCronPeriodByName(pollPeriod), { 32 | accountIds : getAccountIds() 33 | }, () => 34 | { 35 | if (isPolling()) { 36 | return; 37 | } 38 | 39 | /* 40 | * Get the parameters we passed for this job 41 | */ 42 | let params = scheduler.getJobParameters(name); 43 | if (typeof params.accountIds === 'undefined') { 44 | return; 45 | } 46 | 47 | pollingMethod(params.accountIds); 48 | }); 49 | }; 50 | 51 | /** 52 | * @param {string[]} accountIds 53 | */ 54 | export function pollingMethod(accountIds: string[]) 55 | { 56 | let lastPoll = getLastPoll(); 57 | 58 | /* 59 | * For each of the accounts, poll for 60 | * notifications since we last polled. 61 | */ 62 | accountIds.forEach(id => dispatch(pollSinceNotifications( 63 | id, getAccountToken(id), lastPoll, true 64 | ))); 65 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/HawkEye/IHawkEyeConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IHawkEyeConfig 3 | { 4 | github: IHawkEyeConfigGitHub; 5 | 6 | appAlerts: IHawkEyeConfigAppAlerts; 7 | }; 8 | 9 | interface IHawkEyeConfigGitHub 10 | { 11 | clientId: string; 12 | 13 | clientSecret: string; 14 | 15 | scopes: string[]; 16 | 17 | webUrl: string; 18 | }; 19 | 20 | interface IHawkEyeConfigAppAlerts 21 | { 22 | showFor: number; 23 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IState.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | 12 | interface IState 13 | { 14 | app: IStateApp; 15 | 16 | setup: IStateSetup; 17 | 18 | accounts: IStateAccounts; 19 | 20 | settings: IStateSettings; 21 | 22 | appAlerts: IStateAppAlerts; 23 | 24 | repositories: IStateRepositories; 25 | 26 | notifications: IStateNotifications; 27 | 28 | authentication: IStateAuthentication; 29 | 30 | notificationFilters: IStateNotificationFilters; 31 | 32 | repositoryMuteFilters: IStateRepositoryMuteFilters; 33 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IStateAccounts.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface IStateAccounts 4 | { 5 | [accountId: string]: IStateAccountsAccount; 6 | }; 7 | 8 | interface IStateAccountsAccount 9 | { 10 | token: string; 11 | 12 | gitHubUser: IGitHubUser; 13 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IStateApp.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IStateApp 3 | { 4 | currentAccountId: number; 5 | 6 | isPolling: boolean; 7 | 8 | lastPoll: string; 9 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IStateAppAlerts.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface IStateAppAlerts 4 | { 5 | alerts: IAppAlert[]; 6 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IStateAuthentication.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IStateAuthentication 3 | { 4 | isAuthenticating: boolean; 5 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IStateNotificationFilters.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IStateNotificationFilters 3 | { 4 | [accountId: string]: INotificationFilterSet; 5 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IStateNotifications.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface IStateNotifications 4 | { 5 | [accountId: string]: IStateNotificationsAccountsNotifications; 6 | }; 7 | 8 | interface IStateNotificationsAccountsNotifications 9 | { 10 | [notificationId: string]: IGitHubNotification; 11 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IStateRepositories.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IStateRepositories 3 | { 4 | [repoId: string]: IGitHubRepository; 5 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IStateRepositoryMuteFilters.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IStateRepositoryMuteFilters 3 | { 4 | [accountId: number]: IStateRepositoryMuteFiltersAccount; 5 | }; 6 | 7 | interface IStateRepositoryMuteFiltersAccount 8 | { 9 | [repoId: string]: IStateRepositoryMuteFiltersAccountRepo; 10 | }; 11 | 12 | interface IStateRepositoryMuteFiltersAccountRepo 13 | { 14 | allowedSubjectTypes: string[]; 15 | 16 | allowedReasons: string[]; 17 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IStateSettings.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IStateSettings 3 | { 4 | colorMode: string; 5 | 6 | pollPeriod: string; 7 | 8 | soundSettings: IStateSettingsSound; 9 | 10 | accountSettings: IStateSettingsAccountSettings; 11 | 12 | notifications: IStateSettingsNotification; 13 | }; 14 | 15 | interface IStateSettingsAccountSettings 16 | { 17 | [accountId: string]: IStateSettingsAccountSettingsItem; 18 | }; 19 | 20 | interface IStateSettingsAccountSettingsItem 21 | { 22 | 23 | }; 24 | 25 | interface IStateSettingsSound 26 | { 27 | newItemsEnabled: boolean; 28 | 29 | alertSuccessEnabled: boolean; 30 | 31 | alertErrorEnabled: boolean; 32 | }; 33 | 34 | interface IStateSettingsNotification 35 | { 36 | doubleClickAction: string; 37 | 38 | confirmBeforeMarkingMultipleAsRead: boolean; 39 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/IState/IStateSetup.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IStateSetup 3 | { 4 | isLoading: boolean; 5 | 6 | showLoading: boolean; 7 | 8 | renderApp: boolean; 9 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/Models/IAppAlert.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IAppAlert 3 | { 4 | id: string; 5 | 6 | index: number; 7 | 8 | status: string; 9 | 10 | message: string; 11 | 12 | show: boolean; 13 | 14 | sticky: boolean; 15 | 16 | stickyActionIcon?: string; 17 | 18 | stickyActionName?: string; 19 | 20 | stickyActionParams?: any; 21 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/Models/IGitHubNotification.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface IGitHubNotification 4 | { 5 | id: number; 6 | 7 | reason: string; 8 | 9 | unread: boolean; 10 | 11 | updatedAt: string; 12 | 13 | lastReadAt: string; 14 | 15 | url: string; 16 | 17 | repository: IGitHubRepository; 18 | 19 | subject: IGitHubNotificationSubject; 20 | }; 21 | 22 | interface IGitHubNotificationSubject 23 | { 24 | title: string; 25 | 26 | url: string; 27 | 28 | latestCommentUrl: string; 29 | 30 | type: string; 31 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/Models/IGitHubNotificationFilterSet.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IGitHubNotificationFilterSet 3 | { 4 | read: number; 5 | 6 | subjectTypes: IGitHubNotificationFilterSetStringFilter[]; 7 | 8 | reasonTypes: IGitHubNotificationFilterSetStringFilter[]; 9 | 10 | repositories: IGitHubNotificationFilterSetRepository[]; 11 | }; 12 | 13 | interface IGitHubNotificationFilterSetStringFilter 14 | { 15 | name: string; 16 | 17 | count: number; 18 | }; 19 | 20 | interface IGitHubNotificationFilterSetRepository 21 | { 22 | repository: IGitHubRepository; 23 | 24 | count: number; 25 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/Models/IGitHubRepository.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface IGitHubRepository 4 | { 5 | id: number; 6 | 7 | name: string; 8 | 9 | fullName: string; 10 | 11 | private: boolean; 12 | 13 | htmlUrl: string; 14 | 15 | owner: IGitHubUser; 16 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/Models/IGitHubUser.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IGitHubUser 3 | { 4 | id: number; 5 | 6 | avatarUrl: string; 7 | 8 | name: string; 9 | 10 | email: string; 11 | 12 | createdAt: string; 13 | 14 | username: string; 15 | }; -------------------------------------------------------------------------------- /src/js/Interfaces/Models/INotificationFilterSet.ts: -------------------------------------------------------------------------------- 1 | 2 | interface INotificationFilterSet 3 | { 4 | read: boolean; 5 | 6 | subjectType: string[]; 7 | 8 | reasonType: string[]; 9 | 10 | repository: number[]; 11 | 12 | }; -------------------------------------------------------------------------------- /src/js/Reducers/Accounts.ts: -------------------------------------------------------------------------------- 1 | import Reducify from 'Helpers/State/Reducify'; 2 | import ActionConstants from 'Constants/Actions/Index'; 3 | 4 | import * as omit from 'lodash/omit'; 5 | import * as objectAssign from 'object-assign'; 6 | 7 | const initialState: IStateAccounts = { 8 | 9 | }; 10 | 11 | let reducingMethods = { 12 | [ActionConstants.accounts.ADD_ACCOUNT] : (state: IStateAccounts, action) => 13 | { 14 | return objectAssign({}, state, { 15 | [action.user.id] : ({ 16 | token : action.token, 17 | gitHubUser : action.user 18 | } as IStateAccountsAccount) 19 | }); 20 | }, 21 | [ActionConstants.accounts.REMOVE_ACCOUNT] : (state: IStateAccounts, action) => 22 | { 23 | return objectAssign({}, omit(state, action.accountId)); 24 | } 25 | }; 26 | 27 | export default Reducify(initialState, reducingMethods); -------------------------------------------------------------------------------- /src/js/Reducers/App.ts: -------------------------------------------------------------------------------- 1 | import Reducify from 'Helpers/State/Reducify'; 2 | import ActionConstants from 'Constants/Actions/Index'; 3 | 4 | import * as objectAssign from 'object-assign'; 5 | 6 | const initialState: IStateApp = { 7 | currentAccountId : null, 8 | isPolling : false, 9 | lastPoll : null 10 | }; 11 | 12 | let reducingMethods = { 13 | [ActionConstants.app.SET_CURRENT_ACCOUNT_ID] : (state: IStateApp, action) => 14 | { 15 | return objectAssign({}, state, { 16 | currentAccountId : action.currentAccountId 17 | }); 18 | }, 19 | [ActionConstants.app.SET_APP_IS_POLLING] : (state: IStateApp, action) => 20 | { 21 | return objectAssign({}, state, { 22 | isPolling : action.isPolling 23 | }); 24 | }, 25 | [ActionConstants.app.SET_APP_LAST_POLL] : (state: IStateApp, action) => 26 | { 27 | return objectAssign({}, state, { 28 | lastPoll : action.lastPoll 29 | }); 30 | } 31 | }; 32 | 33 | export default Reducify(initialState, reducingMethods); -------------------------------------------------------------------------------- /src/js/Reducers/AppAlerts.ts: -------------------------------------------------------------------------------- 1 | import Reducify from 'Helpers/State/Reducify'; 2 | import ActionConstants from 'Constants/Actions/Index'; 3 | 4 | import * as objectAssign from 'object-assign'; 5 | 6 | const initialState: IStateAppAlerts = { 7 | alerts : [] 8 | }; 9 | 10 | // @todo: Show/hide to be one reducer and two actions 11 | let reducingMethods = { 12 | [ActionConstants.appAlerts.ADD_APP_ALERT] : (state: IStateAppAlerts, action) => 13 | { 14 | return objectAssign({}, state, { 15 | alerts : [...state.alerts, action.appAlert] 16 | }); 17 | }, 18 | [ActionConstants.appAlerts.REMOVE_APP_ALERT] : (state: IStateAppAlerts, action) => 19 | { 20 | return objectAssign({}, state, { 21 | alerts : state.alerts 22 | .filter(alert => alert.id !== action.appAlertId) 23 | }); 24 | }, 25 | [ActionConstants.appAlerts.SHOW_APP_ALERT] : (state: IStateAppAlerts, action) => 26 | { 27 | return objectAssign({}, state, { 28 | alerts : state.alerts 29 | .map(alert => alert.id === action.appAlertId 30 | ? objectAssign({}, alert, { 31 | show : true 32 | }) 33 | : alert) 34 | }); 35 | }, 36 | [ActionConstants.appAlerts.HIDE_APP_ALERT] : (state: IStateAppAlerts, action) => 37 | { 38 | return objectAssign({}, state, { 39 | alerts : state.alerts 40 | .map(alert => alert.id === action.appAlertId 41 | ? objectAssign({}, alert, { 42 | show : false 43 | }) 44 | : alert) 45 | }); 46 | }, 47 | [ActionConstants.appAlerts.HIDE_ALL_APP_ALERTS] : (state: IStateAppAlerts, action) => 48 | { 49 | return objectAssign({}, state, { 50 | alerts : state.alerts 51 | .map(alert => objectAssign({}, alert, { 52 | show : false 53 | })) 54 | }); 55 | }, 56 | }; 57 | 58 | export default Reducify(initialState, reducingMethods); -------------------------------------------------------------------------------- /src/js/Reducers/Authentication.ts: -------------------------------------------------------------------------------- 1 | import Reducify from 'Helpers/State/Reducify'; 2 | import ActionConstants from 'Constants/Actions/Index'; 3 | 4 | import * as objectAssign from 'object-assign'; 5 | 6 | const initialState: IStateAuthentication = { 7 | isAuthenticating : false 8 | }; 9 | 10 | let reducingMethods = { 11 | [ActionConstants.authentication.SET_IS_AUTHENTICATING] : (state: IStateAuthentication, action) => 12 | { 13 | return objectAssign({}, state, { 14 | isAuthenticating : action.isAuthenticating 15 | }); 16 | } 17 | }; 18 | 19 | export default Reducify(initialState, reducingMethods); -------------------------------------------------------------------------------- /src/js/Reducers/Index.ts: -------------------------------------------------------------------------------- 1 | import App from './App'; 2 | import Setup from './Setup'; 3 | import Accounts from './Accounts'; 4 | import Settings from './Settings'; 5 | import AppAlerts from './AppAlerts'; 6 | import Repositories from './Repositories'; 7 | import Notifications from './Notifications'; 8 | import Authentication from './Authentication'; 9 | import NotificationFilters from './NotificationFilters'; 10 | import RepositoryMuteFilters from './RepositoryMuteFilters'; 11 | 12 | export default { 13 | app : App, 14 | setup : Setup, 15 | accounts : Accounts, 16 | settings : Settings, 17 | appAlerts : AppAlerts, 18 | repositories : Repositories, 19 | notifications : Notifications, 20 | authentication : Authentication, 21 | notificationFilters : NotificationFilters, 22 | repositoryMuteFilters : RepositoryMuteFilters 23 | }; -------------------------------------------------------------------------------- /src/js/Reducers/Repositories.ts: -------------------------------------------------------------------------------- 1 | import Reducify from 'Helpers/State/Reducify'; 2 | import ActionConstants from 'Constants/Actions/Index'; 3 | 4 | import * as omit from 'lodash/omit'; 5 | import * as objectAssign from 'object-assign'; 6 | 7 | const initialState: IStateRepositories = { 8 | 9 | }; 10 | 11 | let reducingMethods = { 12 | [ActionConstants.repositories.ADD_REPOSITORY] : (state: IStateRepositories, action: { repository: IGitHubRepository; }) => 13 | { 14 | return objectAssign({}, state, { 15 | [action.repository.id] : action.repository 16 | }); 17 | } 18 | }; 19 | 20 | export default Reducify(initialState, reducingMethods); -------------------------------------------------------------------------------- /src/js/Reducers/Settings.ts: -------------------------------------------------------------------------------- 1 | import Reducify from 'Helpers/State/Reducify'; 2 | 3 | import { cronPeriods } from 'Constants/Lang/Date'; 4 | import ActionConstants from 'Constants/Actions/Index'; 5 | import { 6 | colorModes, 7 | defaultPollPeriod, 8 | notificationDoubleClickActions 9 | } from 'Constants/Models/Settings'; 10 | 11 | 12 | import * as objectAssign from 'object-assign'; 13 | 14 | const initialState: IStateSettings = { 15 | colorMode : colorModes.dark, 16 | pollPeriod : defaultPollPeriod, 17 | accountSettings : {}, 18 | soundSettings : { 19 | newItemsEnabled : true, 20 | alertSuccessEnabled : true, 21 | alertErrorEnabled : true 22 | }, 23 | notifications : { 24 | doubleClickAction : notificationDoubleClickActions.open, 25 | confirmBeforeMarkingMultipleAsRead : true 26 | } 27 | }; 28 | 29 | let reducingMethods = { 30 | [ActionConstants.settings.SET_SETTINGS_VALUE] : (state: IStateSettings, action) => 31 | { 32 | return objectAssign({}, state, { 33 | [action.key] : action.value 34 | }); 35 | }, 36 | [ActionConstants.settings.SET_SOUND_SETTINGS_ENABLED] : (state: IStateSettings, action: { key: string; 37 | enabled: boolean; }) => 38 | { 39 | return objectAssign({}, state, { 40 | soundSettings : objectAssign({}, state.soundSettings, { 41 | [action.key] : action.enabled 42 | }) 43 | }); 44 | }, 45 | [ActionConstants.settings.SET_NOTIFICATIONS_DOUBLE_CLICK_ACTION] : (state: IStateSettings, action: { action: string }) => 46 | { 47 | return objectAssign({}, state, { 48 | notifications : objectAssign({}, state.notifications, { 49 | doubleClickAction : action.action 50 | }) 51 | }) 52 | }, 53 | [ActionConstants.settings.SET_CONFIRM_BEFORE_MARKING_NOTIFICATIONS_AS_READ] : (state: IStateSettings, action: { confirm: boolean }) => 54 | { 55 | return objectAssign({}, state, { 56 | notifications : objectAssign({}, state.notifications, { 57 | confirmBeforeMarkingMultipleAsRead : action.confirm 58 | }) 59 | }) 60 | }, 61 | [ActionConstants.settings.SET_COLOR_MODE] : (state: IStateSettings, action : { colorMode: string }) => 62 | { 63 | return objectAssign({}, state, { 64 | colorMode : action.colorMode 65 | }); 66 | } 67 | }; 68 | 69 | export default Reducify(initialState, reducingMethods); -------------------------------------------------------------------------------- /src/js/Reducers/Setup.ts: -------------------------------------------------------------------------------- 1 | import Reducify from 'Helpers/State/Reducify'; 2 | import ActionConstants from 'Constants/Actions/Index'; 3 | 4 | import * as objectAssign from 'object-assign'; 5 | 6 | const initialState: IStateSetup = { 7 | isLoading : true, 8 | showLoading : true, 9 | renderApp : false 10 | }; 11 | 12 | let reducingMethods = { 13 | [ActionConstants.setup.SET_SETUP_IS_LOADING] : (state: IStateSetup, action) => 14 | { 15 | return objectAssign({}, state, { 16 | isLoading : action.isLoading 17 | }); 18 | }, 19 | [ActionConstants.setup.SET_SETUP_SHOW_LOADING] : (state: IStateSetup, action) => 20 | { 21 | return objectAssign({}, state, { 22 | showLoading : action.showLoading 23 | }); 24 | }, 25 | [ActionConstants.setup.SET_SETUP_RENDER_APP] : (state: IStateSetup, action) => 26 | { 27 | return objectAssign({}, state, { 28 | renderApp : action.renderApp 29 | }); 30 | } 31 | }; 32 | 33 | export default Reducify(initialState, reducingMethods); -------------------------------------------------------------------------------- /src/js/Routes.ts: -------------------------------------------------------------------------------- 1 | import Index from 'View/Index'; 2 | 3 | import SettingsIndex from 'View/Settings/Index'; 4 | 5 | import SettingsAccountView from 'View/Settings/Accounts/View'; 6 | import SettingsAccountRepositoryMuteFilter from 'View/Settings/Accounts/RepositoryMuteFilters'; 7 | 8 | const routes: ReactRouter.PlainRoute[] = [{ 9 | path : 'settings', 10 | component : SettingsIndex 11 | }, { 12 | path : 'settings/accounts/:accountId', 13 | component : SettingsAccountView 14 | }, { 15 | path : 'settings/accounts/:accountId/repo-mute-filter/:repoId', 16 | component : SettingsAccountRepositoryMuteFilter 17 | } 18 | ]; 19 | 20 | export default routes; -------------------------------------------------------------------------------- /src/js/Scheduler/Interfaces/IScheduler.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IScheduler 3 | { 4 | scheduleJob(time: string, parameters: any, method: () => any): string; 5 | 6 | getJobParameters(name: string): any; 7 | 8 | cancelJob(name: string); 9 | 10 | clearAllJobs(); 11 | }; -------------------------------------------------------------------------------- /src/js/Scheduler/Interfaces/ISchedulerJobs.ts: -------------------------------------------------------------------------------- 1 | //import * as nodeSchedule from 'node-schedule'; 2 | 3 | interface ISchedulerJobs 4 | { 5 | [jobId: string]: ISchedulerJobsJob; 6 | }; 7 | 8 | interface ISchedulerJobsJob 9 | { 10 | parameters: any; 11 | 12 | job: any; 13 | }; -------------------------------------------------------------------------------- /src/js/Scheduler/NodeSchedule.ts: -------------------------------------------------------------------------------- 1 | const windowRequire = window['require'] as NodeRequire; 2 | 3 | import * as nodeSchedule from 'node-schedule'; 4 | 5 | /** 6 | * @returns any 7 | */ 8 | export function getNodeSchedule(): any 9 | { 10 | return windowRequire('node-schedule'); 11 | }; 12 | 13 | /** 14 | * @param {string} name 15 | * @param {()=>{}} event 16 | * @returns nodeSchedule 17 | */ 18 | export function scheduleJob(name: string, event: () => {}): nodeSchedule.Job 19 | { 20 | return getNodeSchedule().scheduleJob(name, event) as nodeSchedule.Job; 21 | }; 22 | 23 | /** 24 | * @param {nodeSchedule.Job} job 25 | * @returns boolean 26 | */ 27 | export function cancelJob(job: nodeSchedule.Job): boolean 28 | { 29 | return job.cancel(); 30 | }; 31 | 32 | /** 33 | * @returns nodeSchedule 34 | */ 35 | export function getScheduledJobs(): { [jobName: string]: nodeSchedule.Job; } 36 | { 37 | return getNodeSchedule().scheduledJobs; 38 | }; -------------------------------------------------------------------------------- /src/js/Scheduler/Scheduler.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { 5 | cancelJob, 6 | scheduleJob, 7 | getScheduledJobs 8 | } from './NodeSchedule'; 9 | import { generateId } from 'Helpers/Lang/String'; 10 | 11 | class Scheduler implements IScheduler 12 | { 13 | private jobs: ISchedulerJobs = {}; 14 | 15 | constructor() 16 | { 17 | /* 18 | * Cancel any currently running jobs. This is 19 | * in case of a refresh and not a restart. 20 | */ 21 | this.cancelRunningJobs(); 22 | this.jobs = {}; 23 | } 24 | 25 | /** 26 | * @param {string} time 27 | * @param {any} parameters 28 | * @param {()=>{}} method 29 | * @returns string 30 | */ 31 | public scheduleJob = (time: string, parameters: any, method: () => any): string => 32 | { 33 | let name = this.makeJobName(); 34 | let job = scheduleJob(time, method); 35 | 36 | this.jobs[name] = { 37 | job : job, 38 | parameters : parameters 39 | } as ISchedulerJobsJob; 40 | 41 | return name; 42 | }; 43 | 44 | /** 45 | * @param {string} name 46 | * @returns any 47 | */ 48 | public getJobParameters = (name: string): any => 49 | { 50 | return typeof this.jobs[name] !== 'undefined' 51 | ? this.jobs[name].parameters 52 | : null; 53 | } 54 | 55 | /** 56 | * @param {string} name 57 | */ 58 | public cancelJob = (name: string) => 59 | { 60 | if (typeof this.jobs[name] === 'undefined') { 61 | return; 62 | } 63 | 64 | this.jobs[name].job.cancel(); 65 | } 66 | 67 | /** 68 | */ 69 | public clearAllJobs = () => 70 | { 71 | this.cancelRunningJobs(); 72 | this.jobs = {}; 73 | } 74 | 75 | /** 76 | * @returns string 77 | */ 78 | private makeJobName = (): string => 79 | { 80 | let name = generateId(); 81 | while (typeof this.jobs[name] !== 'undefined') { 82 | name = generateId(); 83 | } 84 | 85 | return name; 86 | }; 87 | 88 | /** 89 | */ 90 | private cancelRunningJobs = () => 91 | { 92 | let jobs = getScheduledJobs(); 93 | 94 | Object.keys(jobs) 95 | .map(k => jobs[k].cancel()); 96 | }; 97 | }; 98 | 99 | export default Scheduler; -------------------------------------------------------------------------------- /src/js/Services/GitHubAccountsService.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | class GitHubAccountsService implements IGitHubAccountsService 5 | { 6 | private gitHub: IGitHub; 7 | 8 | constructor(gitHub: IGitHub) 9 | { 10 | this.gitHub = gitHub; 11 | } 12 | }; 13 | 14 | export default GitHubAccountsService; -------------------------------------------------------------------------------- /src/js/Services/GitHubAuthenticationService.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { 5 | getGitHubScopes, 6 | getGitHubClientId, 7 | getGitHubClientSecret 8 | } from 'Helpers/Services/GitHub'; 9 | 10 | class GitHubAuthenticationService implements IGitHubAuthenticationService 11 | { 12 | private gitHub: IGitHub; 13 | 14 | constructor(gitHub: IGitHub) 15 | { 16 | this.gitHub = gitHub; 17 | } 18 | 19 | /** 20 | * @returns Promise 21 | */ 22 | public generateOAuthUrl = (): Promise => 23 | { 24 | return this.gitHub 25 | .authentication 26 | .generateOAuthUrl(getGitHubClientId(), getGitHubScopes()); 27 | 28 | } 29 | 30 | /** 31 | * @param {string} code 32 | * @returns Promise 33 | */ 34 | public authenticateAccessToken = (code: string): Promise => 35 | { 36 | return new Promise((resolve, reject) => 37 | { 38 | this.gitHub 39 | .authentication 40 | .authenticateAccessToken(getGitHubClientId(), getGitHubClientSecret(), code) 41 | .then(res => resolve(res), 42 | err => reject(err)); 43 | }); 44 | }; 45 | 46 | /** 47 | * @param {string} token 48 | * @returns Promise 49 | */ 50 | public getAuthenticatedUser = (token: string): Promise => 51 | { 52 | return new Promise((resolve, reject) => 53 | { 54 | this.gitHub 55 | .users 56 | .getAuthenticatedUser(token) 57 | .then(res => resolve(res), 58 | err => reject(err)); 59 | }); 60 | } 61 | }; 62 | 63 | export default GitHubAuthenticationService; -------------------------------------------------------------------------------- /src/js/Services/Interfaces/IGitHubAccountsService.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IGitHubAccountsService 3 | { 4 | 5 | }; -------------------------------------------------------------------------------- /src/js/Services/Interfaces/IGitHubAuthenticationService.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IGitHubAuthenticationService 3 | { 4 | generateOAuthUrl(): Promise; 5 | 6 | authenticateAccessToken(code: string): Promise; 7 | 8 | getAuthenticatedUser(token: string): Promise; 9 | }; -------------------------------------------------------------------------------- /src/js/Services/Interfaces/IGitHubNotificationsService.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IGitHubNotificationsService 3 | { 4 | getNotifications(token: string, 5 | page?: number, 6 | all?: boolean, 7 | participating?: boolean, 8 | since?: string, 9 | before?: string): Promise; 10 | 11 | getAllNotificationsSince(token: string, since: string, all?: boolean): Promise; 12 | 13 | getAllNotificationsBefore(token: string, before: string, all?: boolean): Promise; 14 | 15 | markNotificationAsThread(token: string, threadId: string): Promise; 16 | }; -------------------------------------------------------------------------------- /src/js/View/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | import { registerUpdateAvailableHandler } from 'Electron/Tasks/App'; 5 | import { registerPreferencesMenuControl } from 'Electron/Tasks/Settings'; 6 | import { registerMarkNotificationAsReadSuccess } from 'Electron/Tasks/Notification'; 7 | 8 | import AppBar from './Components/AppBar/Index'; 9 | import AppAlerts from './Components/AppAlerts/Index'; 10 | 11 | class App extends React.Component 12 | { 13 | render() 14 | { 15 | return ( 16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 | 24 |
25 | {this.props.children} 26 |
27 |
28 |
29 | ); 30 | } 31 | 32 | componentDidMount() 33 | { 34 | registerUpdateAvailableHandler(); 35 | registerPreferencesMenuControl(); 36 | registerMarkNotificationAsReadSuccess(); 37 | } 38 | }; 39 | 40 | export default App; -------------------------------------------------------------------------------- /src/js/View/Components/AppAlerts/Index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import AppAlert from './AppAlert'; 5 | 6 | interface IAppAlertsProps 7 | { 8 | appAlerts?: IAppAlert[]; 9 | }; 10 | 11 | class AppAlerts extends React.Component 12 | { 13 | render() 14 | { 15 | return ( 16 |
17 | {this.props.appAlerts 18 | .map(a => )} 20 |
21 | ); 22 | } 23 | }; 24 | 25 | export default connect<{}, {}, IAppAlertsProps>( 26 | (state: IState, props: IAppAlertsProps) => ({ 27 | appAlerts : state.appAlerts.alerts 28 | }) 29 | )(AppAlerts); -------------------------------------------------------------------------------- /src/js/View/Components/AppBar/Account.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import createMenu from 'Electron/Menus/Accounts'; 4 | 5 | import { 6 | Btn, 7 | ProfilePicture 8 | } from 'View/Ui/Index'; 9 | 10 | interface IAppBarAccountProps 11 | { 12 | account: IStateAccountsAccount; 13 | 14 | currentAccountId: number; 15 | 16 | className?: string; 17 | 18 | onClick?(account: IStateAccountsAccount); 19 | }; 20 | 21 | class AppBarAccount extends React.Component 22 | { 23 | static defaultProps = { 24 | onClick : () => {} 25 | }; 26 | 27 | handleRightClick(e) 28 | { 29 | e.preventDefault(); 30 | 31 | try { 32 | createMenu(this.props.account.gitHubUser.id) 33 | .popup(e.clientX, e.clientY); 34 | } catch (e) {} 35 | } 36 | 37 | render() 38 | { 39 | return ( 40 | 46 |
47 | 48 |
49 |
50 | ); 51 | } 52 | }; 53 | 54 | export default AppBarAccount; -------------------------------------------------------------------------------- /src/js/View/Components/AppBar/Index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { dispatch } from 'Helpers/State/Store'; 5 | import { isMac } from 'Helpers/System/Electron'; 6 | import { switchAccount } from 'Actions/UIActions/App'; 7 | 8 | import { 9 | Icon, 10 | BtnTo, 11 | Scroll, 12 | } from 'View/Ui/Index'; 13 | 14 | import Account from './Account'; 15 | 16 | interface IAppBarProps 17 | { 18 | app?: IStateApp; 19 | 20 | accounts?: IStateAccountsAccount[]; 21 | }; 22 | 23 | class AppBar extends React.Component 24 | { 25 | handleAccountClick(account: IStateAccountsAccount) 26 | { 27 | dispatch(switchAccount(account.gitHubUser.id)); 28 | } 29 | 30 | render() 31 | { 32 | return ( 33 |
37 |
38 |
39 |
40 |
41 |
42 | 43 | {this.props.accounts 44 | .map((account, i, a) => 45 | ( 46 |
51 | 54 |
55 | ))} 56 |
57 |
58 |
59 |
60 | 62 | 64 | 65 |
66 |
67 |
68 |
69 |
70 | ); 71 | } 72 | }; 73 | 74 | export default connect( 75 | (state: IState, props: IAppBarProps) => ({ 76 | app : state.app, 77 | accounts : Object.keys(state.accounts) 78 | .map(id => state.accounts[id]) 79 | 80 | }) 81 | )(AppBar as any); 82 | -------------------------------------------------------------------------------- /src/js/View/Components/GenericError.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | BtnTo, 5 | CenteredBox 6 | } from 'View/Ui/Index'; 7 | 8 | interface IGenericErrorProps 9 | { 10 | title: string; 11 | 12 | description: string; 13 | 14 | buttonText: string; 15 | 16 | buttonUrl: string; 17 | }; 18 | 19 | class GenericError extends React.Component 20 | { 21 | render() 22 | { 23 | return ( 24 | 25 |

26 | {this.props.title} 27 |

28 |

29 | {this.props.description} 30 |

31 | 33 | {this.props.buttonText} 34 | 35 |
36 | ); 37 | }; 38 | }; 39 | 40 | export default GenericError; -------------------------------------------------------------------------------- /src/js/View/Components/NoAccounts/Index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { dispatch } from 'Helpers/State/Store'; 5 | import { handleAddAccountClick } from 'Actions/UIActions/Accounts'; 6 | 7 | import { 8 | Btn, 9 | CenteredBox 10 | } from 'View/Ui/Index'; 11 | 12 | interface INoAccountsProps 13 | { 14 | authentication?: IStateAuthentication; 15 | }; 16 | 17 | class NoAccounts extends React.Component 18 | { 19 | render() 20 | { 21 | return ( 22 | 23 |

24 | {'Welcome to Hawk Eye'} 25 |

26 |

27 | {'Let\'s get started by adding a GitHub account.'} 28 |

29 | 31 | {'Add an Account'} 32 | 33 |
34 | ); 35 | } 36 | 37 | handleAddAccountClick() 38 | { 39 | if (this.props.authentication.isAuthenticating) { 40 | return; 41 | } 42 | 43 | dispatch(handleAddAccountClick()); 44 | } 45 | }; 46 | 47 | export default connect<{}, {}, INoAccountsProps>( 48 | (state: IState, props: INoAccountsProps) => ({ 49 | authentication : state.authentication 50 | }) 51 | )(NoAccounts); -------------------------------------------------------------------------------- /src/js/View/Components/NoNotifications/Index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { dispatch } from 'Helpers/State/Store'; 4 | import { clearFilters } from 'Actions/NotificationFilters'; 5 | 6 | import { 7 | Btn, 8 | CenteredBox 9 | } from 'View/Ui/Index'; 10 | 11 | interface INoNotificationsProps 12 | { 13 | accountId: number; 14 | }; 15 | 16 | class NoNotifications extends React.Component 17 | { 18 | handleClearFiltersClick() 19 | { 20 | dispatch(clearFilters(this.props.accountId)); 21 | } 22 | 23 | render() 24 | { 25 | return ( 26 | 27 |

28 | {'Quiver Zero!'} 29 |

30 |

31 | {'Hah, get it? But well done!'} 32 |

33 | 35 | {'Clear Filters'} 36 | 37 |
38 | ); 39 | } 40 | }; 41 | 42 | export default NoNotifications; -------------------------------------------------------------------------------- /src/js/View/Components/NotificationFilters/NotificationFilterRepositoryFilter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import createMenu from 'Electron/Menus/Repository'; 4 | 5 | import { Btn } from 'View/Ui/Index'; 6 | 7 | interface INotificationFilterRepositoryFilterProps 8 | { 9 | accountId: number; 10 | 11 | repositoryFilters: IGitHubNotificationFilterSetRepository[]; 12 | 13 | className?: string; 14 | 15 | getTitle(): string; 16 | 17 | getFilterTitle(filter: IGitHubNotificationFilterSetRepository): string; 18 | 19 | getFilterIsActive(filter: IGitHubNotificationFilterSetRepository): boolean; 20 | 21 | onClick(filter: IGitHubNotificationFilterSetRepository): void; 22 | }; 23 | 24 | class NotificationFilterRepositoryFilter extends React.Component 25 | { 26 | handleRightClick(repository: IGitHubRepository, e) 27 | { 28 | e.preventDefault(); 29 | 30 | try { 31 | createMenu(this.props.accountId, repository) 32 | .popup(e.clientX, e.clientY); 33 | } catch (e) {} 34 | } 35 | 36 | render() 37 | { 38 | return ( 39 |
40 |
41 | 42 |
43 | {this.props.repositoryFilters 44 | .map(filter => 45 | ( 46 |
48 | 54 | {this.props.getFilterTitle(filter)} 55 | {filter.count} 56 | 57 |
58 | ))} 59 |
60 | ); 61 | } 62 | }; 63 | 64 | export default NotificationFilterRepositoryFilter; -------------------------------------------------------------------------------- /src/js/View/Components/NotificationFilters/NotificationFilterStringFilter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Btn } from 'View/Ui/Index'; 4 | 5 | interface INotificationFilterStringFilterProps 6 | { 7 | stringFilters: IGitHubNotificationFilterSetStringFilter[]; 8 | 9 | className?: string; 10 | 11 | getTitle(): string; 12 | 13 | getFilterTitle(filter: IGitHubNotificationFilterSetStringFilter): string; 14 | 15 | getFilterIsActive(filter: IGitHubNotificationFilterSetStringFilter): boolean; 16 | 17 | onClick(filter: IGitHubNotificationFilterSetStringFilter): void; 18 | }; 19 | 20 | class NotificationFilterStringFilter extends React.Component 21 | { 22 | render() 23 | { 24 | return ( 25 |
26 |
27 | 28 |
29 | {this.props.stringFilters 30 | .map(filter => 31 | ( 32 |
34 | 39 | {this.props.getFilterTitle(filter)} 40 | {filter.count} 41 | 42 |
43 | ))} 44 |
45 | ) 46 | } 47 | }; 48 | 49 | export default NotificationFilterStringFilter; -------------------------------------------------------------------------------- /src/js/View/Components/NotificationsList/Index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | AutoSizer, 5 | Collection 6 | } from 'react-virtualized'; 7 | import { Notification } from 'View/Ui/Index'; 8 | 9 | interface INotificationsListProps 10 | { 11 | accountId: string; 12 | 13 | notifications: IGitHubNotification[]; 14 | 15 | disableMarkAsRead?: boolean; 16 | 17 | onClick?(notification: IGitHubNotification); 18 | 19 | onDoubleClick?(notification: IGitHubNotification); 20 | }; 21 | 22 | class NotificationsList extends React.Component 23 | { 24 | static defaultProps = { 25 | onClick : () => {}, 26 | onDoubleClick : () => {}, 27 | }; 28 | 29 | render() 30 | { 31 | return ( 32 | 33 | {({ height, width }) => { 34 | return width === 0 35 | ?
36 | : }} 42 |
43 | ); 44 | } 45 | 46 | cellSizeCalculator(width: number, o: any) 47 | { 48 | return { 49 | width : width, 50 | height : 100, 51 | x : 0, 52 | y : o.index * 100 53 | }; 54 | }; 55 | 56 | renderRow(notifications: IGitHubNotification[], o: any) 57 | { 58 | return ( 59 |
61 | 66 |
67 | ); 68 | }; 69 | }; 70 | 71 | export default NotificationsList; -------------------------------------------------------------------------------- /src/js/View/Components/ViewBar/Index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Link } from 'react-router'; 4 | import { Icon } from 'View/Ui/Index'; 5 | 6 | interface IViewBarProps 7 | { 8 | title: string; 9 | 10 | backLink?: string; 11 | 12 | getLeftContent?(): any; 13 | 14 | getRightContent?(): any; 15 | }; 16 | 17 | class ViewBar extends React.Component 18 | { 19 | render() 20 | { 21 | return ( 22 |
23 |
24 |
25 | {typeof this.props.backLink === 'string' 26 | ? 28 | 29 | 30 | : undefined} 31 | {typeof this.props.getLeftContent === 'function' 32 | ?
33 | {this.props.getLeftContent()} 34 |
35 | : undefined} 36 |

37 | {this.props.title} 38 |

39 | {typeof this.props.getRightContent === 'function' 40 | ?
41 | {this.props.getRightContent()} 42 |
43 | : undefined} 44 |
45 |
46 |
47 | {this.props.children} 48 |
49 |
50 | ); 51 | } 52 | }; 53 | 54 | export default ViewBar; -------------------------------------------------------------------------------- /src/js/View/Container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Provider } from 'react-redux'; 4 | import { Router } from 'react-router'; 5 | 6 | import { colorModes } from 'Constants/Models/Settings'; 7 | 8 | import { AppLoading } from 'View/Ui/Index'; 9 | 10 | interface IContainerProps 11 | { 12 | store: Redux.Store; 13 | 14 | history: ReactRouterRedux.ReactRouterReduxHistory; 15 | 16 | routes: ReactRouter.PlainRoute; 17 | 18 | setup?: IStateSetup; 19 | 20 | settings?: IStateSettings; 21 | }; 22 | 23 | class Container extends React.Component 24 | { 25 | render() 26 | { 27 | return ( 28 |
32 | {this.props.setup.isLoading 33 | ? 34 | : undefined} 35 | {this.props.setup.renderApp 36 | ? ( 37 | 38 | 40 | 41 | ) 42 | : undefined} 43 |
44 | ); 45 | } 46 | }; 47 | 48 | export default connect<{}, {}, IContainerProps>( 49 | (state: IState) => ({ 50 | setup : state.setup, 51 | settings : state.settings 52 | }) 53 | )(Container); -------------------------------------------------------------------------------- /src/js/View/Settings/Index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router'; 3 | import { connect } from 'react-redux'; 4 | 5 | import AppSection from './Sections/App'; 6 | import SoundsSection from './Sections/Sounds'; 7 | import AccountsSection from './Sections/Accounts'; 8 | import FrequencySection from './Sections/Frequency'; 9 | import NotificationsSection from './Sections/Notifications'; 10 | 11 | import { Scroll } from 'View/Ui/Index'; 12 | import ViewBar from 'View/Components/ViewBar/Index'; 13 | 14 | interface ISettingsIndexProps 15 | { 16 | settings: IStateSettings; 17 | }; 18 | 19 | class SettingsIndex extends React.Component 20 | { 21 | render() 22 | { 23 | return ( 24 | 26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | ); 57 | } 58 | }; 59 | 60 | export default connect( 61 | (state: IState) => ({ 62 | settings : state.settings 63 | }) 64 | )(SettingsIndex); -------------------------------------------------------------------------------- /src/js/View/Settings/Sections/Accounts.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { dispatch } from 'Helpers/State/Store'; 5 | import { handleAddAccountClick } from 'Actions/UIActions/Accounts'; 6 | 7 | import { 8 | Btn, 9 | BtnTo 10 | } from 'View/Ui/Index'; 11 | 12 | interface IAccountsSettingsSectionProps 13 | { 14 | settings: IStateSettings; 15 | 16 | accounts?: IStateAccountsAccount[]; 17 | 18 | authentication?: IStateAuthentication; 19 | }; 20 | 21 | class AccountsSettingsSection extends React.Component 22 | { 23 | handleClick() 24 | { 25 | if (this.props.authentication.isAuthenticating) { 26 | return; 27 | } 28 | 29 | dispatch(handleAddAccountClick()); 30 | } 31 | 32 | render() 33 | { 34 | return ( 35 |
36 |
37 | 38 | {this.props.accounts 39 | .map((acc, i) => 40 | ( 41 | 50 | {'@' + acc.gitHubUser.username} 51 | 52 | ))} 53 | 0 55 | ? ' btn--hard-top' 56 | : undefined)} 57 | onClick={this.handleClick.bind(this)}> 58 | {'Add Account'} 59 | 60 |
61 |
62 | ); 63 | } 64 | }; 65 | 66 | export default connect<{}, {}, IAccountsSettingsSectionProps>( 67 | (state: IState) => ({ 68 | authentication : state.authentication, 69 | accounts : Object.keys(state.accounts) 70 | .map(id => state.accounts[id]) 71 | }) 72 | )(AccountsSettingsSection); -------------------------------------------------------------------------------- /src/js/View/Settings/Sections/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { dispatch } from 'Helpers/State/Store'; 4 | import { setColorMode } from 'Actions/Settings'; 5 | import { colorModes } from 'Constants/Models/Settings'; 6 | 7 | import { Toggle } from 'View/Ui/Index'; 8 | 9 | interface IAppSettingsSectionProps 10 | { 11 | settings: IStateSettings; 12 | }; 13 | 14 | class AppSettingsSection extends React.Component 15 | { 16 | handleColorModeChange(mode) 17 | { 18 | dispatch(setColorMode(mode)); 19 | } 20 | 21 | render() 22 | { 23 | return ( 24 |
25 |
26 | 27 | 38 |
39 |
40 | ); 41 | } 42 | }; 43 | 44 | export default AppSettingsSection; -------------------------------------------------------------------------------- /src/js/View/Settings/Sections/Frequency.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { dispatch } from 'Helpers/State/Store'; 4 | import { cronPeriodPrettyNames } from 'Constants/Lang/Date'; 5 | import { configurePollPeriod } from 'Actions/UIActions/Settings'; 6 | 7 | import { Button } from 'View/Ui/Index'; 8 | 9 | interface IFrequencySettingsSectionProps 10 | { 11 | settings: IStateSettings; 12 | }; 13 | 14 | class FrequencySettingsSection extends React.Component 15 | { 16 | render() 17 | { 18 | return ( 19 |
20 |
21 | 24 | {Object.keys(cronPeriodPrettyNames) 25 | .map((name, i, a) => 26 | ( 27 | 45 | ))} 46 |
47 |
48 | ); 49 | } 50 | }; 51 | 52 | export default FrequencySettingsSection; -------------------------------------------------------------------------------- /src/js/View/Settings/Sections/Sounds.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | setNewItemsEnabled, 5 | setAlertErrorEnabled, 6 | setAlertSuccessEnabled 7 | } from 'Actions/Settings'; 8 | import { dispatch } from 'Helpers/State/Store'; 9 | 10 | import { Toggle } from 'View/Ui/Index'; 11 | 12 | interface ISoundSettingsSectionProps 13 | { 14 | settings: IStateSettings; 15 | }; 16 | 17 | class SoundSettingsSection extends React.Component 18 | { 19 | handleNewItemsEnabledChange(value: boolean) 20 | { 21 | dispatch(setNewItemsEnabled(value)); 22 | } 23 | 24 | handleOtherSoundsChange(value: boolean) 25 | { 26 | dispatch(setAlertSuccessEnabled(value)); 27 | dispatch(setAlertErrorEnabled(value)); 28 | } 29 | 30 | render() 31 | { 32 | return ( 33 |
34 |
35 | 36 | 47 |
48 |
49 | 50 | 62 |
63 |
64 | ); 65 | } 66 | }; 67 | 68 | export default SoundSettingsSection; -------------------------------------------------------------------------------- /src/js/View/Ui/Anchor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IAnchorProps 4 | { 5 | className?: string; 6 | 7 | href?: string; 8 | 9 | preventDefault?: boolean; 10 | 11 | onClick?(): void; 12 | 13 | onContextMenu?(): void; 14 | }; 15 | 16 | class Anchor extends React.Component 17 | { 18 | static defaultProps = { 19 | href : '#', 20 | preventDefault : true, 21 | onClick : () => {}, 22 | onContextMenu : () => {} 23 | }; 24 | 25 | handleClick(e) 26 | { 27 | if (this.props.preventDefault) { 28 | e.preventDefault(); 29 | } 30 | 31 | this.props.onClick(); 32 | } 33 | 34 | render() 35 | { 36 | return ( 37 | 41 | {this.props.children} 42 | 43 | ); 44 | } 45 | }; 46 | 47 | export default Anchor; -------------------------------------------------------------------------------- /src/js/View/Ui/AppLoading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | Loader, 5 | CenteredBox 6 | } from 'View/Ui/Index'; 7 | 8 | interface IAppLoadingProps 9 | { 10 | show: boolean; 11 | }; 12 | 13 | const AppLoading: React.SFC = ({ show }) => 14 | ( 15 |
19 | 20 | 22 | 23 |
24 | ); 25 | 26 | export default AppLoading; -------------------------------------------------------------------------------- /src/js/View/Ui/Btn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Anchor } from './Index'; 4 | 5 | interface IBtnProps 6 | { 7 | className?: string; 8 | 9 | children?: any; 10 | 11 | onClick?(): void; 12 | 13 | onContextMenu?(): void; 14 | }; 15 | 16 | const Btn: React.SFC = props => 17 | ( 18 | 24 | {props.children} 25 | 26 | ); 27 | 28 | export default Btn; -------------------------------------------------------------------------------- /src/js/View/Ui/BtnTo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | interface IBtnToProps 5 | { 6 | to: string; 7 | 8 | className?: string; 9 | 10 | children?: any; 11 | }; 12 | 13 | const BtnTo: React.SFC = props => 14 | ( 15 | 20 | {props.children} 21 | 22 | ); 23 | 24 | export default BtnTo; -------------------------------------------------------------------------------- /src/js/View/Ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IButtonProps 4 | { 5 | onClick(): void; 6 | 7 | className?: string; 8 | 9 | children?: any; 10 | }; 11 | 12 | const Button: React.SFC = props => 13 | ( 14 | 21 | ); 22 | 23 | export default Button; -------------------------------------------------------------------------------- /src/js/View/Ui/CenteredBox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface ICenteredBoxProps 4 | { 5 | children?: any; 6 | 7 | childClassName?: string; 8 | }; 9 | 10 | const CenteredBox: React.SFC = props => 11 | ( 12 |
13 |
17 | {props.children} 18 |
19 |
20 | ); 21 | 22 | export default CenteredBox; -------------------------------------------------------------------------------- /src/js/View/Ui/Icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IIconProps 4 | { 5 | icon: string; 6 | 7 | className?: string; 8 | }; 9 | 10 | const Icon: React.SFC = (props) => 11 | ( 12 | 17 | ); 18 | 19 | export default Icon; -------------------------------------------------------------------------------- /src/js/View/Ui/Index.ts: -------------------------------------------------------------------------------- 1 | import Btn from './Btn'; 2 | import Icon from './Icon'; 3 | import BtnTo from './BtnTo'; 4 | import Scroll from './Scroll'; 5 | import Anchor from './Anchor'; 6 | import Button from './Button'; 7 | import Loader from './Loader'; 8 | import Toggle from './Toggle'; 9 | import AppLoading from './AppLoading'; 10 | import CenteredBox from './CenteredBox'; 11 | import Notification from './Notification'; 12 | import RoundedBtnSet from './RoundedBtnSet'; 13 | import ProfilePicture from './ProfilePicture'; 14 | 15 | export { 16 | Btn, 17 | Icon, 18 | BtnTo, 19 | Scroll, 20 | Anchor, 21 | Button, 22 | Loader, 23 | Toggle, 24 | AppLoading, 25 | CenteredBox, 26 | Notification, 27 | RoundedBtnSet, 28 | ProfilePicture 29 | }; -------------------------------------------------------------------------------- /src/js/View/Ui/Loader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface ILoaderProps 4 | { 5 | size?: string; 6 | 7 | default?: boolean 8 | }; 9 | 10 | const Loader: React.SFC = props => 11 | ( 12 |
19 | {'Loading'} 20 |
21 | ); 22 | 23 | export default Loader; -------------------------------------------------------------------------------- /src/js/View/Ui/ProfilePicture.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IProfilePictureProps 4 | { 5 | picture: string; 6 | }; 7 | 8 | const ProfilePicture: React.SFC = ({ picture }) => 9 | ( 10 | 12 | ); 13 | 14 | export default ProfilePicture; -------------------------------------------------------------------------------- /src/js/View/Ui/RoundedBtnSet.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | isLastItem, 5 | hasMinItems, 6 | isFirstItem 7 | } from 'Helpers/Lang/Array'; 8 | 9 | import { Btn } from './Index'; 10 | 11 | interface IRoundedBtnSetProps 12 | { 13 | className?: string; 14 | 15 | buttons: IRoundedBtnSetBtn[]; 16 | }; 17 | 18 | interface IRoundedBtnSetBtn 19 | { 20 | key: string; 21 | 22 | text: string; 23 | 24 | className?: string; 25 | 26 | onClick?(); 27 | }; 28 | 29 | class RoundedBtnSet extends React.Component 30 | { 31 | render() 32 | { 33 | return ( 34 |
35 | {this.props.buttons 36 | .map((btn, i, a) => 37 | { 38 | let className = (btn.className || '') 39 | + (hasMinItems(a, 1) 40 | && isFirstItem(i) 41 | ? ' btn--hard-bottom' 42 | : '') 43 | + (hasMinItems(a, 1) 44 | && isLastItem(a, i) 45 | ? ' btn--hard-top' 46 | : '') 47 | + (hasMinItems(a, 1) 48 | && !isFirstItem(i) 49 | && !isLastItem(a, i) 50 | ? ' btn--hard' 51 | : '') 52 | 53 | return ( 54 | 57 | {btn.text} 58 | 59 | ); 60 | })} 61 |
62 | ); 63 | } 64 | }; 65 | 66 | export default RoundedBtnSet; -------------------------------------------------------------------------------- /src/js/View/Ui/Scroll.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IScrollProps 4 | { 5 | className?: string; 6 | 7 | children?: any; 8 | }; 9 | 10 | const Scroll: React.SFC = props => 11 | ( 12 |
16 |
17 | {props.children} 18 |
19 |
20 | ); 21 | 22 | export default Scroll; -------------------------------------------------------------------------------- /src/js/View/Ui/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IToggleProps 4 | { 5 | value: any; 6 | 7 | options: IToggleOption[]; 8 | 9 | className?: string; 10 | 11 | onChange?(value: any): void; 12 | }; 13 | 14 | interface IToggleOption 15 | { 16 | index: number; 17 | 18 | text: string; 19 | 20 | value: any; 21 | }; 22 | 23 | class Toggle extends React.Component 24 | { 25 | handleOptionClick(value, e) 26 | { 27 | e.preventDefault(); 28 | 29 | this.props.onChange(value); 30 | } 31 | 32 | render() 33 | { 34 | return ( 35 |
39 | {this.props.options 40 | .sort((a, b) => a.index - b.index) 41 | .map((option, i, a)=> 42 | ( 43 | 56 | {option.text} 57 | 58 | ))} 59 |
60 | ); 61 | } 62 | }; 63 | 64 | export default Toggle; -------------------------------------------------------------------------------- /src/scss/base/_fonts.scss: -------------------------------------------------------------------------------- 1 | //@import url(https://fonts.googleapis.com/css?family=Roboto:400,700,500,300); 2 | @import url('https://fonts.googleapis.com/css?family=Noto+Sans:400, 700'); 3 | 4 | /** 5 | * Themify Icon Font 6 | */ 7 | @font-face { 8 | font-family: 'themify'; 9 | src:url('./src/fonts/themify/themify.eot?-fvbane'); 10 | src:url('./src/fonts/themify/themify.eot?#iefix-fvbane') format('embedded-opentype'), 11 | url('./src/fonts/themify/themify.woff?-fvbane') format('woff'), 12 | url('./src/fonts/themify/themify.ttf?-fvbane') format('truetype'), 13 | url('./src/fonts/themify/themify.svg?-fvbane#themify') format('svg'); 14 | 15 | font-weight: normal; 16 | font-style: normal; 17 | } -------------------------------------------------------------------------------- /src/scss/base/_form.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Form Base 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Base form/input styles 6 | */ 7 | 8 | input, textarea, button { 9 | resize: none; 10 | outline: none; 11 | @include border-radius(0); 12 | } -------------------------------------------------------------------------------- /src/scss/base/_images.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Images 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Base image styles 6 | */ 7 | 8 | img { 9 | display: block; 10 | } -------------------------------------------------------------------------------- /src/scss/base/_lists.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Base Lists 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Base list styles and standardising 6 | */ 7 | 8 | /* 9 | * Reset lists and make them consistent 10 | */ 11 | ul, ol { 12 | margin-bottom: 0; 13 | padding-left: 1em; 14 | } 15 | 16 | ul { 17 | list-style-type: disc; 18 | } 19 | 20 | ol { 21 | list-style-type: decimal; 22 | 23 | ol { 24 | list-style-type: lower-alpha; 25 | } 26 | } 27 | 28 | /* 29 | * Clean List, no funny business 30 | */ 31 | .clean-list { 32 | padding-left: 0; 33 | list-style-type: none; 34 | } 35 | 36 | /** 37 | * Inline List 38 | */ 39 | .inline-list li { 40 | display: inline; 41 | } -------------------------------------------------------------------------------- /src/scss/base/_page.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Page Base 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Base page styles 6 | */ 7 | 8 | * { @include box-sizing(border-box); } 9 | 10 | html, body { 11 | height: 100%; 12 | min-height: 100%; 13 | } 14 | 15 | body, input, textarea, button { 16 | font: { 17 | size: $base-font-size; 18 | family: $base-body-font; 19 | weight: $body-font-weight; 20 | } 21 | 22 | -webkit-font-smoothing: antialiased; 23 | } 24 | 25 | body { 26 | min-width: $min-page-width; 27 | } 28 | 29 | /* 30 | * Disable text selection across the app 31 | */ 32 | :not(input):not(textarea), 33 | :not(input):not(textarea)::after, 34 | :not(input):not(textarea)::before { 35 | -webkit-user-select: none; 36 | user-select: none; 37 | cursor: default; 38 | } 39 | input, button, textarea, :focus { 40 | outline: none; // You should add some other style for :focus to help UX/a11y 41 | } 42 | 43 | a:not([draggable=true]), img:not([draggable=true]) { 44 | -webkit-user-drag: none; 45 | user-drag: none; /* Technically not supported in Electron yet */ 46 | 47 | i { 48 | cursor: default; 49 | } 50 | } 51 | a[href^="http://"], 52 | a[href^="https://"], 53 | a[href^="ftp://"] { 54 | -webkit-user-drag: auto; 55 | user-drag: auto; /* Technically not supported in Electron yet */ 56 | } -------------------------------------------------------------------------------- /src/scss/base/_typography.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Typography 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Typography base 6 | */ 7 | 8 | /* 9 | * Paragraphs 10 | */ 11 | p { 12 | line-height: $text-line-height; 13 | } 14 | 15 | /* 16 | * Anchors 17 | */ 18 | a { 19 | font-size: inherit; 20 | text-decoration: none; 21 | 22 | &:hover { 23 | cursor: pointer; 24 | } 25 | } 26 | 27 | /* 28 | * Headings 29 | */ 30 | @include headings() { 31 | font-weight: $header-font-weight; 32 | line-height: $header-line-height; 33 | } 34 | 35 | h1 { font-size: get-font-size(alpha); } 36 | h2 { font-size: get-font-size(beta); } 37 | h3 { font-size: get-font-size(gamma); } 38 | h4 { font-size: get-font-size(delta); } 39 | h5 { font-size: get-font-size(epsilon); } 40 | h6 { font-size: get-font-size(zeta); } 41 | 42 | /* 43 | * Labels 44 | */ 45 | label { 46 | display: block; 47 | font: { 48 | size: $label-size; 49 | weight: $label-font-weight; 50 | }; 51 | text-transform: $label-text-transform; 52 | } -------------------------------------------------------------------------------- /src/scss/components/_app-alert.scss: -------------------------------------------------------------------------------- 1 | .app-alert-container { 2 | left: 0; 3 | top: -25px; 4 | width: 100%; 5 | height: 25px; 6 | z-index: 9999999; 7 | position: absolute; 8 | } 9 | 10 | .app-alert { 11 | left: 0; 12 | top: 0px; 13 | width: 100%; 14 | height: 25px; 15 | padding: 0 10px; 16 | position: absolute; 17 | @include transition(top .25s ease); 18 | } 19 | 20 | .app-alert__inner { 21 | width: 100%; 22 | height: 25px; 23 | text-align: center; 24 | @include border-radius(0 0 5px 5px); 25 | 26 | p { 27 | color: white; 28 | font-size: 13px; 29 | line-height: 25px; 30 | font-weight: bold; 31 | } 32 | } 33 | 34 | .app-alert--show { 35 | top: 25px; 36 | } 37 | 38 | .app-alert-pad { 39 | padding: 0 10px; //@hack 40 | } 41 | 42 | // Statuses 43 | .app-alert--success .app-alert__inner { background: $background-success; } 44 | .app-alert--warning .app-alert__inner { background: $background-warning; } 45 | .app-alert--error .app-alert__inner { background: $background-error; } 46 | 47 | /* 48 | * Actions 49 | */ 50 | .app-alert__inner__action { 51 | float: left; 52 | width: 25px; 53 | height: 25px; 54 | display: block; 55 | color: white; 56 | font-size: 12px; 57 | line-height: 25px; 58 | text-align: center; 59 | @include transition(background .25s ease); 60 | 61 | i { 62 | 63 | } 64 | 65 | &:hover { 66 | background: trans-dark(.2); 67 | } 68 | } 69 | 70 | .app-alert__inner__action--last { 71 | @include border-radius(0 0 5px 0); 72 | } -------------------------------------------------------------------------------- /src/scss/components/_app.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 100%; 3 | height: 100%; 4 | position: relative; 5 | @include transition(background .25s ease); 6 | } 7 | 8 | .app__loading { 9 | top: 0; 10 | left: 0; 11 | opacity: 0; 12 | width: 100%; 13 | height: 100%; 14 | position: absolute; 15 | z-index: 9999999999; // lol 16 | background: $app-loading-background; 17 | @include transition(opacity .25s ease); 18 | } 19 | 20 | .app__loading--show { 21 | opacity: 1; 22 | } 23 | 24 | .app--dark { background: $app-dark-background; } 25 | .app--light { background: $app-light-background; } -------------------------------------------------------------------------------- /src/scss/components/_btn.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | border: 0; 3 | margin: 0; 4 | width: 100%; 5 | display: block; 6 | text-align: center; 7 | 8 | cursor: pointer!important; 9 | font-size: $button-font-size; 10 | padding: $button-padding; 11 | text-shadow: $button-text-shadow; 12 | text-transform: $button-text-transform; 13 | @include border-radius($button-border-radius); 14 | @include transition(all $button-transition-speed); 15 | } 16 | 17 | /* 18 | * Square Sizes 19 | */ 20 | @each $size-name, $size in $btn-square-sizes { 21 | .btn--square-#{$size-name} { 22 | padding: 0; 23 | width: $size; 24 | height: $size; 25 | line-height: $size; 26 | } 27 | } 28 | 29 | /* 30 | * Backgrounds 31 | */ 32 | @each $bg-name, $bgs in $btn-backgrounds { 33 | .btn--#{$bg-name} { 34 | background: nth($bgs, 1); 35 | 36 | &:hover { 37 | background: nth($bgs, 2); 38 | } 39 | } 40 | } 41 | 42 | .btn--transparent { 43 | background: transparent; 44 | 45 | &:hover { 46 | background: transparent; 47 | } 48 | } 49 | 50 | .btn--error { 51 | color: white; 52 | } 53 | 54 | /* 55 | * Pill 56 | */ 57 | .btn--pill { 58 | @extend .truncate; 59 | padding: $btn-pill-padding; 60 | font-size: $btn-pill-font-size; 61 | text-align: $btn-pill-text-align; 62 | text-transform: $btn-pill-text-transform; 63 | } 64 | 65 | .btn--pill-has-count { 66 | position: relative; 67 | padding-right: 60px; 68 | } 69 | 70 | .btn--pill__count { 71 | top: 3px; 72 | right: 20px; 73 | width: 25px; 74 | height: 17px; 75 | display: block; 76 | font-size: 10px; 77 | line-height: 17px; 78 | text-align: center; 79 | position: absolute; 80 | @include border-radius(10px); 81 | } 82 | 83 | /* 84 | * Small Icon 85 | */ 86 | .btn--small-icon { 87 | padding: 0; 88 | width: 16px; 89 | height: 16px; 90 | text-align: center; 91 | @include border-radius(3px); 92 | 93 | i { 94 | font-size: 8px; 95 | line-height: 16px; 96 | font-weight: bold; 97 | } 98 | } 99 | 100 | /* 101 | * Hard Corners 102 | */ 103 | .btn--hard { @include border-radius(0); } 104 | .btn--hard-top { @include border-radius(0 0 $button-border-radius $button-border-radius); } 105 | .btn--hard-bottom { @include border-radius($button-border-radius $button-border-radius 0 0); } 106 | .btn--hard-left { @include border-radius($button-border-radius 0 0 $button-border-radius); } 107 | .btn--hard-right { @include border-radius(0 $button-border-radius $button-border-radius 0); } 108 | 109 | /* 110 | * Mods 111 | */ 112 | .btn .profile-picture { 113 | @include border-radius($button-border-radius - 1px); 114 | } -------------------------------------------------------------------------------- /src/scss/components/_loader.scss: -------------------------------------------------------------------------------- 1 | .loader, 2 | .loader:after { 3 | width: 40px; 4 | height: 40px; 5 | @include border-radius(50%); 6 | } 7 | .loader { 8 | margin: 0 auto; 9 | font-size: 10px; 10 | position: relative; 11 | text-indent: -9999em; 12 | 13 | border-top-color: rgba(255, 255, 255, 0.2); 14 | border-right-color: rgba(255, 255, 255, 0.2); 15 | border-bottom-color: rgba(255, 255, 255, 0.2); 16 | border-left-color: white; 17 | 18 | border-top-width: 5px; 19 | border-right-width: 5px; 20 | border-bottom-width: 5px; 21 | border-left-width: 5px; 22 | 23 | border-top-style: solid; 24 | border-right-style: solid; 25 | border-bottom-style: solid; 26 | border-left-style: solid; 27 | 28 | -webkit-transform: translateZ(0); 29 | -ms-transform: translateZ(0); 30 | transform: translateZ(0); 31 | -webkit-animation: load8 1.1s infinite linear; 32 | animation: load8 1.1s infinite linear; 33 | } 34 | 35 | @-webkit-keyframes load8 { 36 | 0% { 37 | -webkit-transform: rotate(0deg); 38 | transform: rotate(0deg); 39 | } 40 | 100% { 41 | -webkit-transform: rotate(360deg); 42 | transform: rotate(360deg); 43 | } 44 | } 45 | @keyframes load8 { 46 | 0% { 47 | -webkit-transform: rotate(0deg); 48 | transform: rotate(0deg); 49 | } 50 | 100% { 51 | -webkit-transform: rotate(360deg); 52 | transform: rotate(360deg); 53 | } 54 | } 55 | 56 | @each $size-name, $size in $loader-sizes { 57 | .loader--#{$size-name}, 58 | .loader--#{$size-name}:after { 59 | width: $size; 60 | height: $size; 61 | } 62 | } 63 | 64 | .loader--small { 65 | border-top-width: 2px; 66 | border-right-width: 2px; 67 | border-bottom-width: 2px; 68 | border-left-width: 2px; 69 | } 70 | 71 | .loader--default { 72 | border-top-color: rgba(255, 255, 255, 0.2)!important; 73 | border-right-color: rgba(255, 255, 255, 0.2)!important; 74 | border-bottom-color: rgba(255, 255, 255, 0.2)!important; 75 | border-left-color: white!important; 76 | } -------------------------------------------------------------------------------- /src/scss/components/_no-outline.scss: -------------------------------------------------------------------------------- 1 | .no-outline * { 2 | outline: none!important; 3 | } -------------------------------------------------------------------------------- /src/scss/components/_notification.scss: -------------------------------------------------------------------------------- 1 | 2 | .notification { 3 | width: 100%; 4 | height: 100%; 5 | position: relative; 6 | @include transition(background .25s ease); 7 | } 8 | 9 | .notification__text { 10 | display: block; 11 | display: -webkit-box; 12 | 13 | max-height: 14 * 1.5 * 2px; 14 | font-size: 14px; 15 | line-height: 1.3em; 16 | margin-bottom: 5px; 17 | -webkit-line-clamp: 2; 18 | -webkit-box-orient: vertical; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | } 22 | 23 | .notification-check { 24 | @include transition(color .25s ease); 25 | } -------------------------------------------------------------------------------- /src/scss/components/_profile-picture.scss: -------------------------------------------------------------------------------- 1 | .profile-picture { 2 | @include border-radius(5px); 3 | } -------------------------------------------------------------------------------- /src/scss/components/_toggle.scss: -------------------------------------------------------------------------------- 1 | .toggle { 2 | width: 100%; 3 | display: inline-block; 4 | @include border-radius($toggle-border-radius); 5 | } 6 | 7 | .toggle__option { 8 | width: 50%; 9 | text-align: center; 10 | font: { 11 | size: $toggle-option-font-size; 12 | weight: $toggle-option-font-weight; 13 | } 14 | display: inline-block; 15 | padding: $toggle-option-padding; 16 | text-transform: $toggle-option-text-transform; 17 | @include transition(all .25s ease); 18 | } 19 | 20 | .toggle__option--first { 21 | @include border-radius($toggle-border-radius 0 0 $toggle-border-radius); 22 | } 23 | 24 | .toggle__option--last { 25 | @include border-radius(0 $toggle-border-radius $toggle-border-radius 0); 26 | } -------------------------------------------------------------------------------- /src/scss/components/_view-bar.scss: -------------------------------------------------------------------------------- 1 | //@todo: Variable this up 2 | .view-bar { 3 | width: 100%; 4 | height: 100%; 5 | position: relative; 6 | @extend .app-drag; 7 | } 8 | 9 | .view-bar__title { 10 | font-size: 14px; 11 | line-height: 40px; 12 | text-align: center; 13 | 14 | } 15 | 16 | .view-bar__back-link { 17 | top: 0; 18 | left: 0; 19 | width: 35px; 20 | height: 40px; 21 | text-align: right; 22 | position: absolute; 23 | 24 | i { 25 | width: 35px; 26 | height: 40px; 27 | font-size: 18px; 28 | line-height: 40px; 29 | } 30 | } 31 | 32 | .view-bar__left-content { 33 | top: 0; 34 | left: 0; 35 | width: 40px; 36 | height: 40px; 37 | position: absolute; 38 | } 39 | 40 | .view-bar__right-content { 41 | top: 0; 42 | right: 0; 43 | width: 40px; 44 | height: 40px; 45 | position: absolute; 46 | } -------------------------------------------------------------------------------- /src/scss/directives/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Breakpoint Directives 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Methods and helpers for working 6 | * with our defined breakpoints. 7 | */ 8 | 9 | @mixin breakpoint($breakpoint-name) { 10 | 11 | @if $breakpoint-name == mobile { 12 | @media (max-width: $mobile-max-breakpoint) { 13 | @content 14 | } 15 | } 16 | 17 | @else if $breakpoint-name == tablet { 18 | @media (min-width: $tablet-min-breakpoint) and (max-width: $tablet-max-breakpoint) { 19 | @content; 20 | } 21 | } 22 | 23 | @else if $breakpoint-name == desktop { 24 | @media (min-width: $desktop-min-breakpoint) { 25 | @content; 26 | } 27 | } 28 | 29 | @else if $breakpoint-name == max { 30 | @media (min-width: $max-min-breakpoint) { 31 | @content; 32 | } 33 | } 34 | 35 | } 36 | 37 | @function prefix-breakpoint-class($class-name) { 38 | @return $class-name + $breakpoint-separator; 39 | }; -------------------------------------------------------------------------------- /src/scss/directives/_font-sizes.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Font Size Directives 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Methods to associate with our 6 | * font-size values. 7 | */ 8 | 9 | /* 10 | * get-font-size 11 | * 12 | * Retrive the font size for a font size name 13 | */ 14 | @function get-font-size($size-name: 'alpha') { 15 | @return map-get($font-sizes, $size-name); 16 | } -------------------------------------------------------------------------------- /src/scss/directives/_grid.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Grid Directives 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Helpers for grid methods 6 | */ 7 | 8 | @function get-grid-gutter($gutter-name: default) { 9 | @return map-get($grid-gutters, $gutter-name); 10 | }; 11 | 12 | @mixin silent-relative() { 13 | @if $grid-use-silent == true { 14 | position: relative; 15 | }; 16 | }; -------------------------------------------------------------------------------- /src/scss/directives/_headings.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Headings Directives 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Headings directives 6 | */ 7 | 8 | /** 9 | * headings-each 10 | * 11 | * Duplicate a block of code, for headings 12 | * from x to y. 13 | */ 14 | @mixin headings-each($from: 1, $to: 6){ 15 | @for $i from $from through $to{ 16 | h#{$i}{ 17 | @content; 18 | }; 19 | }; 20 | }; 21 | 22 | /** 23 | * headings 24 | */ 25 | @mixin headings() { 26 | h1, h2, h3, 27 | h4, h5, h6 { 28 | @content; 29 | } 30 | } 31 | 32 | @mixin heading($i) { 33 | h#{$i} { 34 | @content; 35 | } 36 | } -------------------------------------------------------------------------------- /src/scss/directives/_keyframes.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Keyframes Directives 3 | * Jan 2106 - Andrew Hathaway 4 | * 5 | * Directives to help with keyframes 6 | */ 7 | 8 | /** 9 | * Keyframes mixin 10 | * 11 | * Usage: @include keyframes([name], { CONTENT }); 12 | */ 13 | @mixin keyframes($name) { 14 | @-webkit-keyframes #{$name} { 15 | @content; 16 | } 17 | 18 | @-moz-keyframes #{$name} { 19 | @content; 20 | } 21 | 22 | @-ms-keyframes #{$name} { 23 | @content; 24 | } 25 | 26 | @-o-keyframes #{$name} { 27 | @content; 28 | } 29 | 30 | @keyframes #{$name} { 31 | @content; 32 | } 33 | } -------------------------------------------------------------------------------- /src/scss/directives/_pushes.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Push Directives 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Methods associated with our values 6 | * for pushes. 7 | */ 8 | 9 | /** 10 | * get-push-value 11 | * 12 | * Retreive the push value for a spacingName 13 | */ 14 | @function get-push-value($spacing-name: 'alpha') { 15 | @return map-get($base-push-values, $spacing-name); 16 | } -------------------------------------------------------------------------------- /src/scss/directives/_softs.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Soft Directives 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Methods associated with our values 6 | * for soft values. 7 | */ 8 | 9 | /** 10 | * get-soft-value 11 | * 12 | * Retreive the sot value for a spacingName 13 | */ 14 | @function get-soft-value($spacing-name: 'alpha') { 15 | @return map-get($base-soft-values, $spacing-name); 16 | } -------------------------------------------------------------------------------- /src/scss/directives/_transparents.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Transparent Directives 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Methods to help with transparancies 6 | */ 7 | 8 | /** 9 | * trans-dark 10 | * 11 | * Simple method to help with dark overlays etc 12 | */ 13 | @function trans-dark($percentage: 0.5) { 14 | @return rgba(0, 0, 0, $percentage); 15 | } 16 | 17 | /** 18 | * trans-light 19 | * 20 | * Simple method to help with light overlays etc 21 | */ 22 | @function trans-light($percentage: 0.5) { 23 | @return rgba(255, 255, 255, $percentage); 24 | } -------------------------------------------------------------------------------- /src/scss/generic/_app-drag.scss: -------------------------------------------------------------------------------- 1 | .app-drag { 2 | -ms-overflow-style: scrollbar; 3 | -webkit-app-region:drag; 4 | } 5 | 6 | .app-drag--none { 7 | -webkit-app-region: no-drag; 8 | } -------------------------------------------------------------------------------- /src/scss/generic/_backgrounds.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Backgrounds 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Background helpers 6 | */ 7 | 8 | @mixin generate-backgrounds($pre-class-name: null) { 9 | .#{$pre-class-name}bg--none { background: transparent!important; } 10 | .#{$pre-class-name}bg--white { background: white!important; } 11 | .#{$pre-class-name}bg--black { background: black!important; } 12 | 13 | .#{$pre-class-name}bg--success { background: $background-success!important; } 14 | .#{$pre-class-name}bg--error { background: $background-error! important; } 15 | .#{$pre-class-name}bg--warning { background: $background-warning!important; } 16 | 17 | @each $name, $color in $backgrounds { 18 | .#{$pre-class-name}bg--#{$name} { background: $color!important; } 19 | } 20 | } 21 | 22 | /* 23 | * Generate defafult backgrounds 24 | */ 25 | @include generate-backgrounds(); 26 | 27 | /* 28 | * Generate backgrounds for any 29 | * breakpoints in the variables 30 | */ 31 | @each $breakpoint-name in $breakpoint-backgrounds { 32 | @include breakpoint($breakpoint-name) { 33 | @include generate-backgrounds(prefix-breakpoint-class($breakpoint-name)); 34 | } 35 | }; -------------------------------------------------------------------------------- /src/scss/generic/_borders.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Borders 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Border helpers 6 | */ 7 | 8 | @mixin generate-borders($pre-class-name: null) { 9 | .#{$pre-class-name}border-solid { border-style: solid!important; } 10 | .#{$pre-class-name}border-dashed { border-style: dashed!important; } 11 | 12 | .#{$pre-class-name}border-single { border-width: 1px!important; } 13 | 14 | .#{$pre-class-name}border-top-single { border-top-width: 1px!important; } 15 | .#{$pre-class-name}border-bottom-single { border-bottom-width: 1px!important; } 16 | 17 | .#{$pre-class-name}border-mid-grey { border-color: #D6D6D6!important; } 18 | .#{$pre-class-name}border-light-grey { border-color: #EEE!important; } 19 | .#{$pre-class-name}border-black { border-color: black!important; } 20 | } 21 | 22 | /* 23 | * Generate defafult borders 24 | */ 25 | @include generate-borders(); 26 | 27 | /* 28 | * Generate borders for any 29 | * breakpoints in the variables 30 | */ 31 | @each $breakpoint-name in $breakpoint-borders { 32 | @include breakpoint($breakpoint-name) { 33 | @include generate-borders(prefix-breakpoint-class($breakpoint-name)); 34 | } 35 | }; -------------------------------------------------------------------------------- /src/scss/generic/_clearfix.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Clearfix 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * The clear fix object. Extend this 6 | * instead of having it appear in markup 7 | * majority of the time. 8 | */ 9 | 10 | .clear { 11 | 12 | &:after { 13 | content: ''; 14 | display: table; 15 | clear: both; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/scss/generic/_displays.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Displays 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Display helpers 6 | */ 7 | 8 | @mixin generate-displays($pre-class-name: null) { 9 | .#{$pre-class-name}display--hidden { display: none!important; } 10 | .#{$pre-class-name}display--block { display: block!important; } 11 | .#{$pre-class-name}display--inline { display: inline!important; } 12 | .#{$pre-class-name}display--inline-block { display: inline-block!important; } 13 | .#{$pre-class-name}display--table { display: table!important; } 14 | .#{$pre-class-name}display--table-cell { display: table-cell!important; } 15 | } 16 | 17 | /* 18 | * Generate default displays 19 | */ 20 | @include generate-displays(); 21 | 22 | /* 23 | * Generate displays for any breakpoints 24 | * setup in the variables. 25 | */ 26 | @each $breakpoint-name in $breakpoint-displays { 27 | @include breakpoint($breakpoint-name) { 28 | @include generate-displays(prefix-breakpoint-class($breakpoint-name)); 29 | }; 30 | }; -------------------------------------------------------------------------------- /src/scss/generic/_floats.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Floats 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Float helpers 6 | */ 7 | 8 | @mixin generate-floats($pre-class-name: null) { 9 | .#{$pre-class-name}float--left { float: left!important; } 10 | .#{$pre-class-name}float--right { float: right!important; } 11 | .#{$pre-class-name}float--none { float: none!important; } 12 | } 13 | 14 | /* 15 | * Generate default floats 16 | */ 17 | @include generate-floats(); 18 | 19 | /* 20 | * Generate floats for any breakpoints 21 | * setup in the variables. 22 | */ 23 | @each $breakpoint-name in $breakpoint-floats { 24 | @include breakpoint($breakpoint-name) { 25 | @include generate-floats(prefix-breakpoint-class($breakpoint-name)); 26 | } 27 | } -------------------------------------------------------------------------------- /src/scss/generic/_hards.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Hards 3 | * March 2016 - Andrew Hathaway 4 | * 5 | * Generate hard (padding) rules for values. 6 | */ 7 | @mixin generate-hard-rules($class-pre-name) { 8 | .#{$class-pre-name}hard { padding: 0!important; } 9 | .#{$class-pre-name}hard--top { padding-top: 0!important; } 10 | .#{$class-pre-name}hard--bottom { padding-bottom: 0!important; } 11 | .#{$class-pre-name}hard--left { padding-left: 0!important; } 12 | .#{$class-pre-name}hard--right { padding-right: 0!important; } 13 | .#{$class-pre-name}hard--ends { padding-top: 0!important; 14 | padding-bottom: 0!important; } 15 | .#{$class-pre-name}hard--sides { padding-left: 0!important; 16 | padding-right: 0!important; } 17 | }; 18 | 19 | /* 20 | * Generate the standard hards 21 | */ 22 | @include generate-hard-rules(null); 23 | 24 | /* 25 | * For each breakpoint, generate the hards. 26 | */ 27 | @each $breakpoint-name in $breakpoint-hards { 28 | @include breakpoint($breakpoint-name) { 29 | @include generate-hard-rules(prefix-breakpoint-class($breakpoint-name)); 30 | } 31 | }; -------------------------------------------------------------------------------- /src/scss/generic/_heights.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Heights 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Height helpers 6 | */ 7 | 8 | @mixin generate-heights($pre-class-name: null) { 9 | /* 10 | * None-fractional heights 11 | */ 12 | 13 | .#{$pre-class-name}height--full { height: 100%!important; } 14 | .#{$pre-class-name}height--auto { height: auto!important; } 15 | 16 | .#{$pre-class-name}height--20 { height: 20px!important; } 17 | .#{$pre-class-name}height--30 { height: 30px!important; } 18 | .#{$pre-class-name}height--40 { height: 40px!important; } 19 | .#{$pre-class-name}height--80 { height: 80px!important; } 20 | .#{$pre-class-name}height--90 { height: 90px!important; } 21 | .#{$pre-class-name}height--100 { height: 100px!important; } 22 | .#{$pre-class-name}height--300 { height: 300px!important; } 23 | .#{$pre-class-name}height--340 { height: 340px!important; } 24 | .#{$pre-class-name}height--400 { height: 400px!important; } 25 | } 26 | 27 | /* 28 | * Generate defafult heights 29 | */ 30 | @include generate-heights(); 31 | 32 | /* 33 | * Generate heights for any 34 | * breakpoints in the variables 35 | */ 36 | @each $breakpoint-name in $breakpoint-heights { 37 | @include breakpoint($breakpoint-name) { 38 | @include generate-heights(prefix-breakpoint-class($breakpoint-name)); 39 | } 40 | }; -------------------------------------------------------------------------------- /src/scss/generic/_highers.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Highers 3 | * March 2016 - Andrew Hathaway 4 | * 5 | * Generate z-index helpers 6 | */ 7 | .higher { z-index: $higher; } 8 | 9 | @mixin generate-higher-rule($higher-name: 'alpha', $sizing-value: $higher) { 10 | .higher--#{$higher-name} { z-index: $sizing-value; } 11 | }; 12 | 13 | @each $sizing-name, $sizing-value in $higher-sizes { 14 | @include generate-higher-rule($sizing-name, $sizing-value); 15 | }; -------------------------------------------------------------------------------- /src/scss/generic/_line-heights.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Line Heights 3 | * May 2016 - Andrew Hathaway 4 | * 5 | * Line Heights 6 | */ 7 | 8 | @mixin generate-line-heights($pre-class-name: null) { 9 | .#{$pre-class-name}line-height--none { line-height: 1em!important; } 10 | .#{$pre-class-name}line-height--1-3 { line-height: 1.3em!important; } 11 | 12 | .#{$pre-class-name}line-height--20 { line-height: 20px!important; } 13 | .#{$pre-class-name}line-height--25 { line-height: 25px!important; } 14 | .#{$pre-class-name}line-height--30 { line-height: 30px!important; } 15 | .#{$pre-class-name}line-height--40 { line-height: 40px!important; } 16 | } 17 | 18 | @include generate-line-heights(); 19 | 20 | @each $breakpoint-name in $breakpoint-line-heights { 21 | @include breakpoint($breakpoint-name) { 22 | @include generate-line-heights(prefix-breakpoint-class($breakpoint-name)); 23 | } 24 | }; -------------------------------------------------------------------------------- /src/scss/generic/_max-widths.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Max Widths 3 | * Mar 2016 - Andrew Hathaway 4 | * 5 | * Max-Width helpers 6 | */ 7 | 8 | @mixin generate-max-widths($pre-class-name: null) { 9 | /* 10 | * None-fractional max-widths 11 | */ 12 | .#{$pre-class-name}max-width--200 { max-width: 200px!important; } 13 | .#{$pre-class-name}max-width--240 { max-width: 240px!important; } 14 | .#{$pre-class-name}max-width--290 { max-width: 290px!important; } 15 | .#{$pre-class-name}max-width--450 { max-width: 450px!important; } 16 | .#{$pre-class-name}max-width--540 { max-width: 540px!important; } 17 | 18 | .#{$pre-class-name}max-width--35p { max-width: 35%!important; } 19 | .#{$pre-class-name}max-width--50p { max-width: 50%!important; } 20 | .#{$pre-class-name}max-width--75p { max-width: 75%!important; } 21 | } 22 | 23 | /* 24 | * Generate defafult max-widths 25 | */ 26 | @include generate-max-widths(); 27 | 28 | /* 29 | * Generate max-widths for any 30 | * breakpoints in the variables 31 | */ 32 | @each $breakpoint-name in $breakpoint-max-widths { 33 | @include breakpoint($breakpoint-name) { 34 | @include generate-max-widths(prefix-breakpoint-class($breakpoint-name)); 35 | } 36 | }; -------------------------------------------------------------------------------- /src/scss/generic/_min-heights.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Min Height 3 | * Mar 2016 - Andrew Hathaway 4 | * 5 | * Min-Height helpers 6 | */ 7 | 8 | @mixin generate-min-heights($pre-class-name: null) { 9 | /* 10 | * None-fractional min-heights 11 | */ 12 | .#{$pre-class-name}min-height--full { min-height: 100%!important; } 13 | } 14 | 15 | /* 16 | * Generate defafult min-heights 17 | */ 18 | @include generate-min-heights(); 19 | 20 | /* 21 | * Generate min-heights for any 22 | * breakpoints in the variables 23 | */ 24 | @each $breakpoint-name in $breakpoint-min-heights { 25 | @include breakpoint($breakpoint-name) { 26 | @include generate-min-heights(prefix-breakpoint-class($breakpoint-name)); 27 | } 28 | }; -------------------------------------------------------------------------------- /src/scss/generic/_n-tops.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Negative Tops 3 | * 4 | */ 5 | 6 | @mixin generate-n-tops($pre-class-name: null) { 7 | @each $name, $size in $n-top-sizes { 8 | .#{$pre-class-name}n-top--#{$name} { top: $size!important; } 9 | } 10 | }; 11 | 12 | @include generate-n-tops(); 13 | 14 | @each $breakpoint-name in $breakpoint-n-tops { 15 | @include breakpoint($breakpoint-name) { 16 | @include generate-n-tops(prefix-breakpoint-class($breakpoint-name)); 17 | } 18 | } -------------------------------------------------------------------------------- /src/scss/generic/_overflows.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Overflows 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Overflow helpers 6 | */ 7 | 8 | @mixin generate-overflows($pre-class-name: null) { 9 | .#{$pre-class-name}overflow--visible { overflow: visible!important; } 10 | .#{$pre-class-name}overflow--hidden { overflow: hidden!important; } 11 | .#{$pre-class-name}overflow--scroll { overflow: scroll!important; } 12 | .#{$pre-class-name}overflow--xscroll { overflow-x: scroll!important; } 13 | .#{$pre-class-name}overflow--yscroll { overflow-y: scroll!important; } 14 | } 15 | 16 | /* 17 | * Generate default overflows 18 | */ 19 | @include generate-overflows(); 20 | 21 | /* 22 | * Generate overflows for any breakpoints 23 | * setup in the variables 24 | */ 25 | @each $breakpoint-name in $breakpoint-overflows { 26 | @include breakpoint($breakpoint-name) { 27 | @include generate-overflows(prefix-breakpoint-class($breakpoint-name)); 28 | }; 29 | }; -------------------------------------------------------------------------------- /src/scss/generic/_positionings.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Positionings 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Generate position rules 6 | */ 7 | 8 | @mixin generate-positionings($class-pre-name: null) { 9 | .#{$class-pre-name}position--static { position: static!important; } 10 | .#{$class-pre-name}position--fixed { position: fixed!important; } 11 | .#{$class-pre-name}position--relative { position: relative!important; } 12 | .#{$class-pre-name}position--absolute { position: absolute!important; } 13 | } 14 | 15 | /* 16 | * Generate default positionings 17 | */ 18 | @include generate-positionings(); 19 | 20 | /* 21 | * Generate positionings for any breakpoints 22 | * setup in the variables 23 | */ 24 | @each $breakpoint-name in $breakpoint-positionings { 25 | @include breakpoint($breakpoint-name) { 26 | @include generate-positionings(prefix-breakpoint-class($breakpoint-name)); 27 | } 28 | } -------------------------------------------------------------------------------- /src/scss/generic/_pushes.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Pushes 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Generate push rules for required values 6 | */ 7 | 8 | @mixin generate-push-rules($class-pre-name: null, $spacing-name: 'alpha') { 9 | .#{$class-pre-name}push-#{$spacing-name} { margin: get-push-value($spacing-name)!important; } 10 | .#{$class-pre-name}push-#{$spacing-name}--top { margin-top: get-push-value($spacing-name)!important; } 11 | .#{$class-pre-name}push-#{$spacing-name}--bottom { margin-bottom: get-push-value($spacing-name)!important; } 12 | .#{$class-pre-name}push-#{$spacing-name}--left { margin-left: get-push-value($spacing-name)!important; } 13 | .#{$class-pre-name}push-#{$spacing-name}--right { margin-right: get-push-value($spacing-name)!important; } 14 | .#{$class-pre-name}push-#{$spacing-name}--ends { margin-top: get-push-value($spacing-name)!important; 15 | margin-bottom: get-push-value($spacing-name)!important; } 16 | .#{$class-pre-name}push-#{$spacing-name}--sides { margin-left: get-push-value($spacing-name)!important; 17 | margin-right: get-push-value($spacing-name)!important; } 18 | } 19 | 20 | @mixin generate-push-autos($class-pre-name: null) { 21 | .#{$class-pre-name}push-auto--sides { 22 | margin-left: auto!important; 23 | margin-right: auto!important; 24 | } 25 | } 26 | 27 | /* 28 | * Generate a set of push rules for the 29 | * defined push-values. 30 | */ 31 | @each $spacing-name, $spacing-value in $base-push-values { 32 | @include generate-push-rules(null, $spacing-name); 33 | } 34 | 35 | /* 36 | * Generate default push-auto rules 37 | */ 38 | @include generate-push-autos(); 39 | 40 | /* 41 | * Generate a set of push rules for the breakpoints 42 | * defined in each sets list. Wrap them in a breakpoint 43 | * and prefix the class with the breakpoint name and -. 44 | */ 45 | @each $spacing-name, $breakpoint-list in $breakpoint-pushes { 46 | @each $breakpoint-name in $breakpoint-list { 47 | @include breakpoint($breakpoint-name) { 48 | @include generate-push-rules(prefix-breakpoint-class($breakpoint-name), $spacing-name); 49 | }; 50 | }; 51 | }; 52 | 53 | /* 54 | * Generate the set of push autos for 55 | * breakpoints defined in the variables. 56 | */ 57 | @each $breakpoint-name in $breakpoint-push-autos { 58 | @include breakpoint($breakpoint-name) { 59 | @include generate-push-autos(prefix-breakpoint-class($breakpoint-name)); 60 | }; 61 | }; -------------------------------------------------------------------------------- /src/scss/generic/_round.scss: -------------------------------------------------------------------------------- 1 | .round { @include border-radius($default-border-radius); } -------------------------------------------------------------------------------- /src/scss/generic/_softs.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Softs 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Generate soft (padding) rules for 6 | * required values 7 | */ 8 | @mixin generate-soft-rules($class-pre-name, $spacing-name: 'alpha') { 9 | .#{$class-pre-name}soft-#{$spacing-name} { padding: get-soft-value($spacing-name)!important; } 10 | .#{$class-pre-name}soft-#{$spacing-name}--top { padding-top: get-soft-value($spacing-name)!important; } 11 | .#{$class-pre-name}soft-#{$spacing-name}--bottom { padding-bottom: get-soft-value($spacing-name)!important; } 12 | .#{$class-pre-name}soft-#{$spacing-name}--left { padding-left: get-soft-value($spacing-name)!important; } 13 | .#{$class-pre-name}soft-#{$spacing-name}--right { padding-right: get-soft-value($spacing-name)!important; } 14 | .#{$class-pre-name}soft-#{$spacing-name}--ends { padding-top: get-soft-value($spacing-name)!important; 15 | padding-bottom: get-soft-value($spacing-name)!important; } 16 | .#{$class-pre-name}soft-#{$spacing-name}--sides { padding-left: get-soft-value($spacing-name)!important; 17 | padding-right: get-soft-value($spacing-name)!important; } 18 | } 19 | 20 | /* 21 | * Generate a set of soft rules for the 22 | * defined soft-values. 23 | */ 24 | @each $spacing-name, $spacing-value in $base-soft-values { 25 | @include generate-soft-rules(null, $spacing-name); 26 | }; 27 | 28 | /* 29 | * Generate a set of soft rules for the breakpoints 30 | * defined in each sets list. Wrap them in a breakpoint 31 | * and prefix the class with the breakpoint name and -. 32 | */ 33 | @each $spacing-name, $breakpoint-list in $breakpoint-softs { 34 | @each $breakpoint-name in $breakpoint-list { 35 | @include breakpoint($breakpoint-name) { 36 | @include generate-soft-rules(prefix-breakpoint-class($breakpoint-name), $spacing-name); 37 | }; 38 | }; 39 | }; -------------------------------------------------------------------------------- /src/scss/generic/_text-aligns.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Text Aligns 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Text align helpers 6 | */ 7 | 8 | @mixin generate-text-aligns($pre-class-name: null) { 9 | .#{$pre-class-name}text--center { text-align: center!important; } 10 | .#{$pre-class-name}text--left { text-align: left!important; } 11 | .#{$pre-class-name}text--right { text-align: right!important; } 12 | } 13 | 14 | /* 15 | * Generate default text aligns 16 | */ 17 | @include generate-text-aligns(); 18 | 19 | /* 20 | * Generate text aligns for any 21 | * breakpoints in the variables 22 | */ 23 | @each $breakpoint-name in $breakpoint-text-aligns { 24 | @include breakpoint($breakpoint-name) { 25 | @include generate-text-aligns(prefix-breakpoint-class($breakpoint-name)); 26 | }; 27 | }; -------------------------------------------------------------------------------- /src/scss/generic/_text-colors.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Text Colors 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Text color helpers 6 | */ 7 | 8 | @mixin generate-text-colors($pre-class-name: null) { 9 | .#{$pre-class-name}text--white { color: white!important; } 10 | .#{$pre-class-name}text--error { color: $background-error!important; } 11 | } 12 | 13 | /* 14 | * Generate default text aligns 15 | */ 16 | @include generate-text-colors(); 17 | 18 | /* 19 | * Generate text colors for any 20 | * breakpoints in the variables 21 | */ 22 | @each $breakpoint-name in $breakpoint-text-colors { 23 | @include breakpoint($breakpoint-name) { 24 | @include generate-text-colors(prefix-breakpoint-class($breakpoint-name)); 25 | }; 26 | }; -------------------------------------------------------------------------------- /src/scss/generic/_text-emboss.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Text Embossings 3 | * May 2016 - Andrew Hathaway 4 | * 5 | * Text Embossings 6 | */ 7 | @mixin generate-text-embossings($pre-class-name: null) { 8 | .#{$pre-class-name}text--emboss { text-shadow: 0 1px rgba(0, 0, 0, .4); } 9 | } 10 | 11 | @include generate-text-embossings(); 12 | 13 | @each $breakpoint-name in $breakpoint-text-embossings { 14 | @include breakpoint($breakpoint-name) { 15 | @include generate-text-embossings(prefix-breakpoint-class($breakpoint-name)); 16 | } 17 | }; -------------------------------------------------------------------------------- /src/scss/generic/_text-fonts.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Text Fonts 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Text font helpers 6 | */ 7 | 8 | @mixin generate-text-fonts($pre-class-name: null) { 9 | .#{$pre-class-name}text--primary { font-family: $primary-font!important; } 10 | } 11 | 12 | /* 13 | * Generate default text fonts 14 | */ 15 | @include generate-text-fonts(); 16 | 17 | /* 18 | * Generate text fonts for any 19 | * breakpoints in the variables 20 | */ 21 | @each $breakpoint-name in $breakpoint-text-fonts { 22 | @include breakpoint($breakpoint-name) { 23 | @include generate-text-fonts(prefix-breakpoint-class($breakpoint-name)); 24 | }; 25 | }; -------------------------------------------------------------------------------- /src/scss/generic/_text-sizes.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Text Sizes 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Text size helpers 6 | */ 7 | 8 | @mixin generate-text-sizes($pre-class-name: null) { 9 | @each $size-name, $font-size in $font-sizes { 10 | .#{$pre-class-name}text--#{$size-name} { font-size: $font-size!important; } 11 | }; 12 | }; 13 | 14 | /* 15 | * Generate the default text sizes 16 | */ 17 | @include generate-text-sizes(); 18 | 19 | /* 20 | * Generate a set of text size rules for the breakpoints 21 | * defined in each sets list. Wrap them in a breakpoint 22 | * and prefix them with the class and the breakpoint name. 23 | */ 24 | @each $space-name, $breakpoint-list in $breakpoint-font-sizes { 25 | @each $breakpoint-name in $breakpoint-list { 26 | @include breakpoint($breakpoint-name) { 27 | @include generate-text-sizes(prefix-breakpoint-class($breakpoint-name)); 28 | }; 29 | }; 30 | }; -------------------------------------------------------------------------------- /src/scss/generic/_text-transforms.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Text Transforms 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Text font helpers 6 | */ 7 | 8 | @mixin generate-text-transforms($pre-class-name: null) { 9 | .#{$pre-class-name}text--no-transform { text-transform: none!important; } 10 | .#{$pre-class-name}text--uppercase { text-transform: uppercase!important; } 11 | .#{$pre-class-name}text--lowercase { text-transform: lowercase!important; } 12 | .#{$pre-class-name}text--capitalize { text-transform: capitalize!important; } 13 | } 14 | 15 | /* 16 | * Generate default text transforms 17 | */ 18 | @include generate-text-transforms(); 19 | 20 | /* 21 | * Generate text transforms for any 22 | * breakpoints in the variables 23 | */ 24 | @each $breakpoint-name in $breakpoint-text-transforms { 25 | @include breakpoint($breakpoint-name) { 26 | @include generate-text-transforms(prefix-breakpoint-class($breakpoint-name)); 27 | }; 28 | }; -------------------------------------------------------------------------------- /src/scss/generic/_text-weights.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Text Weights 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Text weight helpers 6 | */ 7 | 8 | @mixin generate-text-weights($pre-class-name: null) { 9 | .#{$pre-class-name}text--bold { font-weight: bold!important; } 10 | .#{$pre-class-name}text--normal { font-weight: normal!important; } 11 | } 12 | 13 | /* 14 | * Generate default text weights 15 | */ 16 | @include generate-text-weights(); 17 | 18 | /* 19 | * Generate text weights for any 20 | * breakpoints in the variables 21 | */ 22 | @each $breakpoint-name in $breakpoint-text-weights { 23 | @include breakpoint($breakpoint-name) { 24 | @include generate-text-weights(prefix-breakpoint-class($breakpoint-name)); 25 | }; 26 | }; -------------------------------------------------------------------------------- /src/scss/generic/_tops.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Negative Tops 3 | * 4 | */ 5 | 6 | @mixin generate-tops($pre-class-name: null) { 7 | @each $name, $size in $top-sizes { 8 | .#{$pre-class-name}top--#{$name} { top: $size!important; } 9 | } 10 | }; 11 | 12 | @include generate-tops(); 13 | 14 | @each $breakpoint-name in $breakpoint-tops { 15 | @include breakpoint($breakpoint-name) { 16 | @include generate-tops(prefix-breakpoint-class($breakpoint-name)); 17 | } 18 | } -------------------------------------------------------------------------------- /src/scss/objects/_cover-height.scss: -------------------------------------------------------------------------------- 1 | .cover-height { 2 | width: 100%; 3 | overflow: hidden; 4 | max-height: 120px; 5 | @include transition(max-height .25s ease); 6 | } 7 | 8 | .cover-height--130 { 9 | max-height: 130px; 10 | } 11 | 12 | .cover-height--show { 13 | max-height: 9000px!important; 14 | } 15 | 16 | .cover-height--to-470.cover-height--show { 17 | max-height: 470px!important; 18 | } -------------------------------------------------------------------------------- /src/scss/objects/_dialog.scss: -------------------------------------------------------------------------------- 1 | 2 | .dialog { 3 | width: 100%; 4 | height: 100%; 5 | display: table; 6 | } 7 | 8 | .dialog__cell { 9 | text-align: center; 10 | display: table-cell; 11 | vertical-align: middle; 12 | } -------------------------------------------------------------------------------- /src/scss/objects/_dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | position: relative; 3 | } 4 | 5 | .dropdown__menu { 6 | top: 100%; 7 | right: 0; 8 | display: none; 9 | position: absolute; 10 | } 11 | 12 | .dropdown--open .dropdown__menu { 13 | display: block; 14 | } 15 | 16 | .dropdown__menu--center { 17 | left: 50%; 18 | right: auto; 19 | } -------------------------------------------------------------------------------- /src/scss/objects/_fixed-overlay.scss: -------------------------------------------------------------------------------- 1 | 2 | .fixed-overlay { 3 | width: 100%; 4 | height: 100%; 5 | position: fixed; 6 | z-index: 99999999999999; 7 | } -------------------------------------------------------------------------------- /src/scss/objects/_flexi-side.scss: -------------------------------------------------------------------------------- 1 | .flexi-side { 2 | width: 100%; 3 | display: table; 4 | position: relative; 5 | } 6 | 7 | .flexi-side__side, 8 | .flexi-side__content { 9 | display: table-cell; 10 | vertical-align: middle; 11 | } 12 | 13 | .flexi-side__side { 14 | height: 100%; 15 | } 16 | 17 | .flexi-side__content { 18 | width: 100%; 19 | } -------------------------------------------------------------------------------- /src/scss/objects/_grid.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Grid System 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * Awesome grid system. Split out 6 | * from CSSWizardry Grids. 7 | */ 8 | 9 | .grid { 10 | list-style: none; 11 | margin: 0; 12 | padding: 0; 13 | margin-left: get-grid-gutter() * -1; 14 | } 15 | 16 | @if $grid-use-markup-fix != true { 17 | /* Opera hack */ 18 | .opera:-o-prefocus, 19 | .grid{ 20 | word-spacing:-0.43em; 21 | } 22 | } 23 | 24 | .grid__item { 25 | display: inline-block; 26 | padding-left: get-grid-gutter(); 27 | vertical-align: top; 28 | 29 | @if $grid-mobile-first == true { 30 | width: 100%; 31 | } 32 | 33 | @if $grid-use-markup-fix != true { 34 | letter-spacing: normal; 35 | word-spacing: normal; 36 | } 37 | } 38 | 39 | /** 40 | * Modifiers 41 | */ 42 | 43 | /* 44 | * Reversed grids allow you to structure your source in the opposite order to 45 | * how your rendered layout will appear. Extends `.grid`. 46 | */ 47 | .grid--rev { 48 | direction: rtl; 49 | text-align: left; 50 | 51 | > .grid__item { 52 | direction: ltr; 53 | text-align: left; 54 | } 55 | } 56 | 57 | /* 58 | * Gutterless grids have all the properties of regular grids, minus any spacing. 59 | * Extends `.grid`. 60 | */ 61 | .grid--full { 62 | margin-left: 0; 63 | 64 | > .grid__item { 65 | padding-left: 0; 66 | } 67 | } 68 | 69 | /* 70 | * Align the entire grid to the right. Extends `.grid`. 71 | */ 72 | .grid--right { 73 | text-align: right; 74 | 75 | > .grid__item { 76 | text-align: left; 77 | } 78 | } 79 | 80 | /* 81 | * Centered grids align grid items centrally without needing to use push or pull 82 | * classes. Extends `.grid`. 83 | */ 84 | .grid--center { 85 | text-align: center; 86 | 87 | > .grid__item { 88 | text-align: left; 89 | } 90 | } 91 | 92 | /* 93 | * Align grid cells vertically (`.grid--middle` or `.grid--bottom`). Extends 94 | * `.grid`. 95 | */ 96 | .grid--middle { 97 | > .grid__item { 98 | vertical-align: middle; 99 | } 100 | } 101 | 102 | .grid--bottom { 103 | > .grid__item { 104 | vertical-align: bottom; 105 | } 106 | } 107 | 108 | /* 109 | * Generate gutter methods for any 110 | * others than the default. 111 | * 112 | * SIDENOTE: WHY CANNOT SASS JUST GET @CONTINUE 113 | */ 114 | @each $gutter-name, $gutter-value in $grid-gutters { 115 | @if $gutter-name != default { 116 | .grid--#{$gutter-name} { 117 | margin-left: -$gutter-value; 118 | 119 | > .grid__item { 120 | padding-left: $gutter-value; 121 | } 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/scss/objects/_hard-bottom.scss: -------------------------------------------------------------------------------- 1 | .hard-bottom { 2 | width: 100%; 3 | height: 100%; 4 | position: relative; //@todo: Mixin these rules? fullRelative() 5 | } 6 | 7 | .hard-bottom__bottom { 8 | bottom: 0; 9 | width: 100%; 10 | position: absolute; 11 | height: $hard-bottom-width; 12 | } 13 | 14 | .hard-bottom__content { 15 | width: 100%; 16 | height: 100%; 17 | padding-bottom: $hard-bottom-width; 18 | } 19 | 20 | @mixin generate-hard-bottoms($post-class-name: null) { 21 | .hard-bottom--#{$post-class-name} { 22 | > .hard-bottom__bottom { height: map-get($hard-bottom-sizes, $post-class-name); } 23 | > .hard-bottom__content { padding-bottom: map-get($hard-bottom-sizes, $post-class-name); } 24 | } 25 | }; 26 | 27 | @each $hard-name, $hard-size in $hard-bottom-sizes { 28 | @include generate-hard-bottoms($hard-name); 29 | } -------------------------------------------------------------------------------- /src/scss/objects/_hard-left.scss: -------------------------------------------------------------------------------- 1 | .hard-left { 2 | width: 100%; 3 | height: 100%; 4 | overflow: hidden; 5 | position: relative; //@todo: Mixin these rules? fullRelative() 6 | } 7 | 8 | .hard-left__left { 9 | top: 0; 10 | left: 0; 11 | bottom: 0; 12 | height: 100%; 13 | position: absolute; 14 | width: $hard-left-width; 15 | @include transition(left .25s ease-in-out); 16 | } 17 | 18 | .hard-left__content { 19 | width: 100%; 20 | height: 100%; 21 | padding-left: $hard-left-width; 22 | @include transition(padding-left .25s ease-in-out); 23 | } 24 | 25 | .hard-left--pull { 26 | > .hard-left__left { left: -$hard-left-width; } 27 | > .hard-left__content { padding-left: 0; } 28 | } 29 | 30 | @mixin generate-hards($post-class-name: null) { 31 | .hard-left--#{$post-class-name} { 32 | > .hard-left__left { width: map-get($hard-left-sizes, $post-class-name); } 33 | > .hard-left__content { padding-left: map-get($hard-left-sizes, $post-class-name)!important; } 34 | 35 | &.hard-left--pull { 36 | > .hard-left__left { left: -map-get($hard-left-sizes, $post-class-name); } 37 | > .hard-left__content { padding-left: 0; } 38 | } 39 | } 40 | }; 41 | 42 | @each $hard-name, $hard-size in $hard-left-sizes { 43 | @include generate-hards($hard-name); 44 | } -------------------------------------------------------------------------------- /src/scss/objects/_hard-right.scss: -------------------------------------------------------------------------------- 1 | .hard-right { 2 | width: 100%; 3 | height: 100%; 4 | overflow: hidden; 5 | position: relative; //@todo: Mixin these rules? fullRelative() 6 | } 7 | 8 | .hard-right--allow-overflow { 9 | overflow: visible; 10 | } 11 | 12 | .hard-right__right { 13 | top: 0; 14 | right: 0; 15 | bottom: 0; 16 | height: 100%; 17 | position: absolute; 18 | width: $hard-right-width; 19 | @include transition(right .25s ease-in-out); 20 | } 21 | 22 | .hard-right__content { 23 | width: 100%; 24 | height: 100%; 25 | padding-right: $hard-right-width; 26 | @include transition(padding-right .25s ease-in-out); 27 | } 28 | 29 | .hard-right--pull { 30 | overflow: hidden; 31 | 32 | > .hard-right__right { right: -$hard-right-width; } 33 | > .hard-right__content { padding-right: 0; } 34 | } 35 | 36 | @mixin generate-hards($post-class-name: null) { 37 | .hard-right--#{$post-class-name} { 38 | > .hard-right__right { width: map-get($hard-right-sizes, $post-class-name); } 39 | > .hard-right__content { padding-right: map-get($hard-right-sizes, $post-class-name)!important; } 40 | 41 | &.hard-right--pull { 42 | > .hard-right__right { right: map-get($hard-right-sizes, $post-class-name); } 43 | > .hard-right__content { padding-right: 0; } 44 | } 45 | } 46 | }; 47 | 48 | @each $hard-name, $hard-size in $hard-right-sizes { 49 | @include generate-hards($hard-name); 50 | } -------------------------------------------------------------------------------- /src/scss/objects/_hard-top.scss: -------------------------------------------------------------------------------- 1 | .hard-top { 2 | width: 100%; 3 | height: 100%; 4 | position: relative; //@todo: Mixin these rules? fullRelative() 5 | } 6 | 7 | .hard-top__top { 8 | top: 0; 9 | top: 0; 10 | bottom: 0; 11 | z-index: 9; 12 | width: 100%; 13 | position: absolute; 14 | height: $hard-top-width; 15 | } 16 | 17 | .hard-top__content { 18 | width: 100%; 19 | height: 100%; 20 | padding-top: $hard-top-width; 21 | } 22 | 23 | @mixin generate-hard-tops($post-class-name: null) { 24 | .hard-top--#{$post-class-name} { 25 | > .hard-top__top { height: map-get($hard-top-sizes, $post-class-name); } 26 | > .hard-top__content { padding-top: map-get($hard-top-sizes, $post-class-name); } 27 | } 28 | }; 29 | 30 | @each $hard-name, $hard-size in $hard-top-sizes { 31 | @include generate-hard-tops($hard-name); 32 | } 33 | 34 | .hard-top--disable { 35 | .hard-top__top { display: none!important; } 36 | .hard-top__content { padding-top: 0px!important; } 37 | } -------------------------------------------------------------------------------- /src/scss/objects/_hideable-left.scss: -------------------------------------------------------------------------------- 1 | 2 | .hideable-left { 3 | width: 100%; 4 | height: 100%; 5 | position: relative; 6 | } 7 | 8 | .hideable-left__left { 9 | top: 0; 10 | left: 0; 11 | bottom: 0; 12 | height: 100%; 13 | width: 200px; 14 | z-index: 9999; 15 | position: absolute; 16 | } 17 | 18 | .hideable-left__content { 19 | width: 100%; 20 | height: 100%; 21 | position: relative; 22 | padding-left: 200px; 23 | }; 24 | 25 | @include breakpoint(mobile) { 26 | .hideable-left__content { 27 | padding-left: 0; 28 | } 29 | .hideable-left__left { 30 | display: none; 31 | } 32 | } -------------------------------------------------------------------------------- /src/scss/objects/_img-scale.scss: -------------------------------------------------------------------------------- 1 | .img-scale { 2 | width: 100%; 3 | height: auto; 4 | } -------------------------------------------------------------------------------- /src/scss/objects/_loading-overlay.scss: -------------------------------------------------------------------------------- 1 | .loading-overlay { 2 | position: relative; 3 | } 4 | 5 | .loading-overlay__overlay { 6 | top: 0; 7 | left: 0; 8 | z-index: 2; 9 | width: 100%; 10 | height: 100%; 11 | position: absolute; 12 | } -------------------------------------------------------------------------------- /src/scss/objects/_media.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Media Object 3 | * Jan 2016 - Andrew Hathaway 4 | * 5 | * The media object: 6 | * http://www.stubbornella.org/content/2010/06/25/the-media-object-saves-hundreds-of-lines-of-code/ 7 | */ 8 | 9 | .media { 10 | @extend .clear; 11 | @extend .display--block; 12 | } 13 | 14 | .media__img { 15 | float: left; 16 | margin-right: 20px; // @todo: replace 17 | } 18 | 19 | .media--right .media__img { 20 | float: right; 21 | margin-left: 20px; // @todo: replace 22 | margin-right: 0; 23 | } 24 | 25 | .media__body { 26 | overflow: hidden; 27 | 28 | &, & > :last-child { 29 | margin-bottom: 0; 30 | } 31 | } -------------------------------------------------------------------------------- /src/scss/objects/_truncate.scss: -------------------------------------------------------------------------------- 1 | .truncate { 2 | width: 100%; 3 | white-space: nowrap; 4 | overflow: hidden; 5 | text-overflow: ellipsis; 6 | } -------------------------------------------------------------------------------- /src/scss/objects/_vscroll.scss: -------------------------------------------------------------------------------- 1 | .vscroll { 2 | width: 100%; 3 | height: 100%; 4 | overflow-y: auto; 5 | position: relative; 6 | } 7 | 8 | .vscroll__content { 9 | top: 0; 10 | left: 0; 11 | right: 0; 12 | bottom: 0; 13 | width: 100%; 14 | height: 100%; 15 | position: absolute; 16 | } -------------------------------------------------------------------------------- /src/scss/vendors/_virtualized.scss: -------------------------------------------------------------------------------- 1 | /* Collection default theme */ 2 | 3 | .ReactVirtualized__Collection { 4 | } 5 | 6 | .ReactVirtualized__Collection__innerScrollContainer { 7 | } 8 | 9 | /* Grid default theme */ 10 | 11 | .ReactVirtualized__Grid { 12 | } 13 | 14 | .ReactVirtualized__Grid__innerScrollContainer { 15 | } 16 | 17 | /* Table default theme */ 18 | 19 | .ReactVirtualized__Table { 20 | } 21 | 22 | .ReactVirtualized__Table__Grid { 23 | } 24 | 25 | .ReactVirtualized__Table__headerRow { 26 | font-weight: 700; 27 | text-transform: uppercase; 28 | display: flex; 29 | flex-direction: row; 30 | align-items: center; 31 | } 32 | .ReactVirtualized__Table__row { 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | } 37 | 38 | .ReactVirtualized__Table__headerTruncatedText { 39 | display: inline-block; 40 | max-width: 100%; 41 | white-space: nowrap; 42 | text-overflow: ellipsis; 43 | overflow: hidden; 44 | } 45 | 46 | .ReactVirtualized__Table__headerColumn, 47 | .ReactVirtualized__Table__rowColumn { 48 | margin-right: 10px; 49 | min-width: 0px; 50 | } 51 | .ReactVirtualized__Table__rowColumn { 52 | text-overflow: ellipsis; 53 | white-space: nowrap; 54 | } 55 | 56 | .ReactVirtualized__Table__headerColumn:first-of-type, 57 | .ReactVirtualized__Table__rowColumn:first-of-type { 58 | margin-left: 10px; 59 | } 60 | .ReactVirtualized__Table__sortableHeaderColumn { 61 | cursor: pointer; 62 | } 63 | 64 | .ReactVirtualized__Table__sortableHeaderIconContainer { 65 | display: flex; 66 | align-items: center; 67 | } 68 | .ReactVirtualized__Table__sortableHeaderIcon { 69 | flex: 0 0 24px; 70 | height: 1em; 71 | width: 1em; 72 | fill: currentColor; 73 | } 74 | 75 | /* List default theme */ 76 | 77 | .ReactVirtualized__List { 78 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions" : { 3 | "target" : "ES5", 4 | "sourceMap" : true, 5 | "module" : "commonjs", 6 | "jsx" : "react", 7 | "baseUrl" : "./src/js" 8 | }, 9 | "exclude" : [ 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /tsconfig.main.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions" : { 3 | "target" : "ES5", 4 | "sourceMap" : true, 5 | "module" : "commonjs", 6 | "moduleResolution" : "node", 7 | "baseUrl" : "./src/js" 8 | }, 9 | "exclude" : [ 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hawkeye", 3 | "dependencies": { 4 | "form-data": "registry:npm/form-data#1.0.0+20160812072516", 5 | "lodash": "registry:npm/lodash#4.0.0+20161015015725", 6 | "redux-persist": "registry:npm/redux-persist#3.2.2+20160817092816", 7 | "request": "registry:dt/request#0.0.0+20161128184003" 8 | }, 9 | "globalDependencies": { 10 | "async": "registry:dt/async#2.0.1+20161211174247", 11 | "electron": "registry:dt/electron#1.4.8+20161220141501", 12 | "electron-window-state": "registry:dt/electron-window-state#2.0.0+20161102134001", 13 | "es2015-symbol": "registry:env/es2015-symbol#1.0.0+20160526151700", 14 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160726072212", 15 | "history": "registry:dt/history#1.8.0+20160316155526", 16 | "moment": "registry:dt/moment#2.11.1+20161010105546", 17 | "node": "registry:dt/node#6.0.0+20161121110008", 18 | "node-schedule": "registry:dt/node-schedule#1.1.0+20161119044246", 19 | "object-assign": "registry:dt/object-assign#4.0.1+20161006140353", 20 | "query-string": "registry:dt/query-string#3.0.0+20161129181732", 21 | "react": "registry:dt/react#0.14.0+20161203201537", 22 | "react-dom": "registry:dt/react-dom#0.14.0+20160412154040", 23 | "react-redux": "registry:dt/react-redux#4.4.0+20160908183346", 24 | "react-router": "registry:dt/react-router#2.0.0+20161117134353", 25 | "react-router-redux": "registry:dt/react-router-redux#4.0.0+20160930121107", 26 | "react-router/history": "registry:dt/react-router/history#2.0.0+20160830150755", 27 | "react-virtualized": "registry:dt/react-virtualized#0.0.0+20161117143210", 28 | "redux": "registry:dt/redux#3.5.2+20160703092728", 29 | "redux-thunk": "registry:dt/redux-thunk#2.1.0+20160703120921", 30 | "whatwg-fetch": "registry:dt/whatwg-fetch#0.0.0+20161120230953", 31 | "whatwg-streams": "registry:dt/whatwg-streams#0.0.0+20161214010628" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry : './src/js/App.ts', 6 | output : { 7 | path : path.resolve('./'), 8 | filename : 'app.min.js' 9 | }, 10 | resolve : { 11 | root : [ 12 | path.resolve('./node_modules'), 13 | path.resolve('./src/js') 14 | ], 15 | extensions : [ 16 | '', 17 | '.webpack.js', 18 | '.web.js', 19 | '.ts', 20 | '.js', 21 | '.tsx' 22 | ] 23 | }, 24 | module : { 25 | loaders : [{ 26 | test : /\.tsx?$/, 27 | loader : 'ts-loader?sourceMap=false&configFileName=tsconfig.json' 28 | }, { 29 | test : /\.css$/, 30 | loader : 'style!css' 31 | }, { 32 | test : /\.(otf|eot|svg|ttf|woff|woff2).*$/, 33 | loader : 'url?limit=8192' 34 | }] 35 | }, 36 | plugins : [ 37 | new webpack.ProvidePlugin({ 38 | 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' 39 | }), 40 | new webpack.optimize.UglifyJsPlugin({ 41 | minimize : true, 42 | output : { 43 | comments : false 44 | }, 45 | compress : { 46 | warnings: false 47 | } 48 | }), 49 | new webpack.DefinePlugin({ 50 | 'process.env.NODE_ENV': '"production"' 51 | }) 52 | ] 53 | }; -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry : './src/js/App.ts', 6 | output : { 7 | path : path.resolve('./'), 8 | filename : 'app.js' 9 | }, 10 | resolve : { 11 | root : [ 12 | path.resolve('./node_modules'), 13 | path.resolve('./src/js') 14 | ], 15 | extensions : [ 16 | '', 17 | '.webpack.js', 18 | '.web.js', 19 | '.ts', 20 | '.js', 21 | '.tsx' 22 | ] 23 | }, 24 | module : { 25 | loaders : [{ 26 | test : /\.tsx?$/, 27 | loader : 'ts-loader?sourceMap=false&configFileName=tsconfig.json' 28 | }, { 29 | test : /\.css$/, 30 | loader : 'style!css' 31 | }, { 32 | test : /\.(otf|eot|svg|ttf|woff|woff2).*$/, 33 | loader : 'url?limit=8192' 34 | }] 35 | }, 36 | plugins : [ 37 | new webpack.ProvidePlugin({ 38 | 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' 39 | }), 40 | new webpack.DefinePlugin({ 41 | 'process.env.NODE_ENV': '"development"' 42 | }) 43 | ] 44 | }; --------------------------------------------------------------------------------